import json
import os
from io import BytesIO
from pathlib import Path
import re
from urllib.parse import urlparse

import requests
from dotenv import load_dotenv
from fastapi import APIRouter, Depends, BackgroundTasks, HTTPException, status, UploadFile, File, Query, Request
from fastapi.responses import RedirectResponse, StreamingResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from datetime import date, datetime, timedelta, timezone
from urllib.parse import quote, urlparse

from app.auth import verify_token, create_access_token
from app.database import SessionLocal
from app.logging_utils import get_logger
from app import crud
from app.models import User, Meeting, Recording, RecurringMeetingCandidate, Transcript
from app.schemas import (
    AdminUserDecisionRequest,
    CalendarSyncSettingRequest,
    CalendarWatchRequest,
    MeetingCreate,
    RecurringCandidateDecisionRequest,
    RecordingResponse,
    ScheduledMeetingCreate,
)
from app.services.pdf_service import build_transcript_pdf
from app.services import admin_dashboard_service, google_calendar_service, recall_service, recall_webhooks
from app.services.meeting_list_service import build_meeting_list

load_dotenv(Path(__file__).resolve().parent / ".env")
load_dotenv()
router = APIRouter()
logger = get_logger(__name__)

STATUS_PRIORITY = {
    "active": 1,
    "failed": 2,
    "completed": 3,
    "scheduled": 4,
    "other": 5,
}

# ─── Google config ────────────────────────────────────────────────────────────

def _require_env(name: str) -> str:
    value = os.getenv(name)
    if not value:
        raise RuntimeError(f"Missing required environment variable: {name}")
    return value


CLIENT_ID = _require_env("GOOGLE_CLIENT_ID")
CLIENT_SECRET = _require_env("GOOGLE_CLIENT_SECRET")
REDIRECT_URI = _require_env("GOOGLE_REDIRECT_URI")
GOOGLE_CALENDAR_WEBHOOK_URL = os.getenv("GOOGLE_CALENDAR_WEBHOOK_URL")
GOOGLE_CALENDAR_SYNC_DAYS = int(os.getenv("GOOGLE_CALENDAR_SYNC_DAYS", "30"))
GOOGLE_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar"
ADMIN_EMAIL = (os.getenv("ADMIN_EMAIL") or "").strip().lower()

# ─── DB dependency ────────────────────────────────────────────────────────────
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


def process_google_webhook(
    channel_id: str,
    channel_token: str | None,
    resource_state: str | None,
    message_number: str | None,
):
    db = SessionLocal()
    try:
        result = google_calendar_service.handle_google_notification(
            db=db,
            channel_id=channel_id,
            channel_token=channel_token,
            resource_state=resource_state,
            message_number=message_number,
        )
        logger.info(
            "Google Calendar webhook processing finished for channel %s. Resource state: %s. Synced: %s. Skipped: %s. Deleted: %s.",
            channel_id,
            resource_state,
            len(result["synced"]),
            len(result["skipped"]),
            len(result["deleted"]),
        )
    except Exception as exc:
        logger.exception(
            "Google Calendar webhook processing failed for channel %s. Resource state: %s. Error: %s.",
            channel_id,
            resource_state,
            exc,
        )
    finally:
        db.close()


def _is_admin_email(email: str | None) -> bool:
    return bool(email and ADMIN_EMAIL and email.strip().lower() == ADMIN_EMAIL)


def _can_access_features(user: User) -> bool:
    return user.role == "admin" or user.approval_status == "approved"


def check_google_calendar_scope(access_token: str) -> bool:
    try:
        response = requests.get(
            "https://www.googleapis.com/oauth2/v1/tokeninfo",
            params={"access_token": access_token},
            timeout=30,
        )
        response.raise_for_status()
        token_info = response.json()
        scopes = (token_info.get("scope") or "").split()
        has_calendar_scope = GOOGLE_CALENDAR_SCOPE in scopes
        logger.info(
            "Google token scope check finished. Calendar access granted: %s. Scopes: %s.",
            has_calendar_scope,
            scopes,
        )
        return has_calendar_scope
    except Exception as exc:
        logger.warning("Google token scope check failed. Error: %s.", exc)
        return False


def start_calendar_watch_if_enabled(db: Session, user: User) -> None:
    if not _can_access_features(user):
        logger.info(
            "Google Calendar watch was not started for user %s because the account status is %s.",
            user.id,
            user.approval_status,
        )
        return
    if not user.calendar_sync_enabled:
        logger.info("Google Calendar watch was not started for user %s because calendar sync is disabled.", user.id)
        return
    if not GOOGLE_CALENDAR_WEBHOOK_URL:
        logger.warning("Google Calendar watch was not started for user %s because the webhook URL is missing.", user.id)
        return

    result = google_calendar_service.ensure_google_watch(
        db=db,
        user=user,
        webhook_url=GOOGLE_CALENDAR_WEBHOOK_URL,
        days_ahead=GOOGLE_CALENDAR_SYNC_DAYS,
        auto_schedule_bot=True,
    )
    watch_state = result.get("watch_state")
    if watch_state:
        logger.info(
            "Google Calendar watch has been started for user %s. Channel id: %s. Expires at: %s.",
            user.id,
            watch_state.channel_id,
            watch_state.expires_at,
        )

# ─── Auth dependency ──────────────────────────────────────────────────────────
security = HTTPBearer()

def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
    db: Session = Depends(get_db),
):
    token = credentials.credentials
    payload = verify_token(token)

    if payload is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token")

    user_id = payload.get("user_id")
    if user_id is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload")

    user = db.query(User).filter(User.id == user_id).first()
    if user is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")

    try:
        start_calendar_watch_if_enabled(db, user)
    except Exception as exc:
        logger.exception("Google Calendar watch setup failed during authentication for user %s. Error: %s.", user.id, exc)

    return user


def get_admin_user(current_user: User = Depends(get_current_user)):
    if current_user.role != "admin":
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
    return current_user


def get_approved_user(current_user: User = Depends(get_current_user)):
    if current_user.role == "admin" or current_user.approval_status == "approved":
        return current_user
    if current_user.approval_status == "denied":
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Your account access has been denied",
        )
    raise HTTPException(
        status_code=status.HTTP_403_FORBIDDEN,
        detail="Your account is pending admin approval",
    )


# ─── Participant helpers ───────────────────────────────────────────────────────

def fetch_meeting_participants(access_token: str, meet_link: str) -> list:
    """Fetch participant names from Google Calendar for this meet link."""
    try:
        from datetime import datetime, timedelta, timezone
        now = datetime.now(timezone.utc)
        time_min = (now - timedelta(days=7)).isoformat()
        time_max = (now + timedelta(days=1)).isoformat()

        stored_code = meet_link.strip("/").split("/")[-1].split("?")[0].lower()
        logger.info("Meeting participant lookup has started for Google Meet code %s.", stored_code)

        cal_list_resp = requests.get(
            "https://www.googleapis.com/calendar/v3/users/me/calendarList",
            headers={"Authorization": f"Bearer {access_token}"}
        )
        calendars = cal_list_resp.json().get("items", [])
        calendar_ids = [c["id"] for c in calendars] or ["primary"]
        logger.info("Meeting participant lookup is checking %s calendars.", len(calendar_ids))

        for cal_id in calendar_ids:
            try:
                response = requests.get(
                    f"https://www.googleapis.com/calendar/v3/calendars/{requests.utils.quote(cal_id)}/events",
                    headers={"Authorization": f"Bearer {access_token}"},
                    params={
                        "timeMin": time_min,
                        "timeMax": time_max,
                        "singleEvents": True,
                        "orderBy": "startTime",
                        "maxResults": 50,
                        "q": stored_code
                    }
                )
                events = response.json().get("items", [])

                for event in events:
                    hangout = event.get("hangoutLink", "")
                    event_code = hangout.strip("/").split("/")[-1].split("?")[0].lower()

                    if stored_code and event_code and stored_code == event_code:
                        attendees = event.get("attendees", [])
                        names = []
                        for a in attendees:
                            name = a.get("displayName") or a.get("email", "").split("@")[0]
                            if name:
                                names.append(name)
                        logger.info("Meeting participant lookup found attendees for code %s: %s.", stored_code, names)
                        return names
            except Exception as e:
                logger.warning("Meeting participant lookup failed for calendar %s. Error: %s.", cal_id, e)
                continue

        logger.info("Meeting participant lookup did not find a matching calendar event for code %s.", stored_code)
        return []

    except Exception as e:
        logger.exception("Meeting participant lookup failed before completion. Error: %s.", e)
        return []


@router.get("/login")
def login(force_consent: bool = Query(False, description="Set to true to force a new refresh token")):
    # Only add prompt=consent if explicitly requested
    prompt_param = "&prompt=consent" if force_consent else ""

    url = (
        "https://accounts.google.com/o/oauth2/v2/auth"
        "?response_type=code"
        f"&client_id={CLIENT_ID}"
        f"&redirect_uri={REDIRECT_URI}"
        "&scope=openid email profile https://www.googleapis.com/auth/calendar"
        "&access_type=offline"
        f"{prompt_param}"
    )
    return RedirectResponse(url)

#
# # ─── OAuth Callback ───────────────────────────────────────────────────────────
# @router.get("/auth/callback")
# def auth_callback(code: str, db: Session = Depends(get_db)):
#     token_url = "https://oauth2.googleapis.com/token"
#
#     data = {
#         "code": code,
#         "client_id": CLIENT_ID,
#         "client_secret": CLIENT_SECRET,
#         "redirect_uri": REDIRECT_URI,
#         "grant_type": "authorization_code",
#     }
#
#     token_response = requests.post(token_url, data=data)
#     token_json = token_response.json()
#     print("TOKEN RESPONSE:", token_json)
#     access_token = token_json.get("access_token")
#     refresh_token = token_json.get("refresh_token")
#
#     if not access_token:
#         return {"error": "Token not generated", "full_response": token_json}
#
#     has_calendar_scope = check_google_calendar_scope(access_token)
#
#     user_info = requests.get(
#         "https://www.googleapis.com/oauth2/v1/userinfo",
#         headers={"Authorization": f"Bearer {access_token}"}
#     ).json()
#     print("USER INFO:", user_info)
#
#     email = user_info.get("email")
#     name = user_info.get("name")
#
#     user = crud.create_or_update_user(db, email, name, access_token, refresh_token=refresh_token)
#     print(
#         f"[auth] Saved Google token for user_id={user.id} email={user.email} "
#         f"has_calendar_scope={has_calendar_scope} refresh_token_received={bool(refresh_token)}"
#     )
#     try:
#         start_calendar_watch_if_enabled(db, user)
#     except Exception as exc:
#         print(f"[google-watch] login setup failed user_id={user.id} error={exc}")
#     jwt_token = create_access_token({"user_id": user.id})
#     frontend_url = os.getenv("FRONTEND_AUTH_CALLBACK_URL", "http://192.168.1.181:3000/auth/callback")
#
#     return RedirectResponse(
#         url=f"{frontend_url}?token={jwt_token}&email={quote(user.email)}&user_id={user.id}"
#     )
    # jwt_token = create_access_token({"user_id": user.id})
    #
    # return {
    #
    #     "email": user.email,
    #     "token": jwt_token
    # }


@router.get("/auth/callback")
def auth_callback(code: str, db: Session = Depends(get_db)):
    token_url = "https://oauth2.googleapis.com/token"
    data = {
        "code": code,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "redirect_uri": REDIRECT_URI,
        "grant_type": "authorization_code",
    }

    token_response = requests.post(token_url, data=data)
    token_json = token_response.json()
    access_token = token_json.get("access_token")
    refresh_token = token_json.get("refresh_token")

    if not access_token:
        return {"error": "Token not generated", "full_response": token_json}

    has_calendar_scope = check_google_calendar_scope(access_token)

    user_info = requests.get(
        "https://www.googleapis.com/oauth2/v1/userinfo",
        headers={"Authorization": f"Bearer {access_token}"}
    ).json()

    email = user_info.get("email")
    name  = user_info.get("name")
    is_admin = _is_admin_email(email)

    #If returning user already has a refresh token stored, preserve it
    existing_user = db.query(User).filter(User.email == email).first()
    if existing_user and existing_user.refresh_token and not refresh_token:
        refresh_token = existing_user.refresh_token

    if not refresh_token:
        logger.warning("Google sign-in for %s did not return a refresh token. The user will be sent back to the consent screen.", email)
        # Redirect the user right back to the login route, but force consent
        return RedirectResponse(url="/login?force_consent=true")

    user = crud.create_or_update_user(
        db, email, name, access_token,
        refresh_token=refresh_token,
        calendar_scope_granted=has_calendar_scope,
        is_admin=is_admin,
    )

    logger.info(
        "Google sign-in finished for user %s with email %s. Calendar access granted: %s. Calendar sync enabled: %s. Refresh token received: %s.",
        user.id,
        user.email,
        has_calendar_scope,
        user.calendar_sync_enabled,
        bool(refresh_token),
    )

    # ✅ Trigger watch only when scope is confirmed — bypasses the DB flag race
    if has_calendar_scope and _can_access_features(user):
        try:
            start_calendar_watch_if_enabled(db, user)
        except Exception as exc:
            logger.exception("Google Calendar watch setup failed after Google sign-in for user %s. Error: %s.", user.id, exc)

    jwt_token = create_access_token({"user_id": user.id})
    frontend_url = os.getenv("FRONTEND_AUTH_CALLBACK_URL", "http://192.168.1.181:3000/auth/callback")

    return RedirectResponse(
        url=f"{frontend_url}?token={jwt_token}&email={quote(user.email)}&user_id={user.id}"
    )


@router.get("/me")
def get_me(
    current_user: User = Depends(get_current_user),
):
    return {
        "success": True,
        "data": {
            "id": current_user.id,
            "email": current_user.email,
            "name": current_user.name,
            "role": current_user.role,
            "approval_status": current_user.approval_status,
            "calendar_sync_enabled": current_user.calendar_sync_enabled,
        },
    }


# ─── Create Meeting ───────────────────────────────────────────────────────────
@router.post("/create-meeting")
def create_meeting(current_user: User = Depends(get_approved_user)):
    logger.info("Instant Google Meet creation was requested by user %s.", current_user.id)
    access_token = current_user.access_token
    start_time = datetime.utcnow()
    end_time = start_time + timedelta(minutes=30)

    event_url = "https://www.googleapis.com/calendar/v3/calendars/primary/events"
    headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}

    data = {
        "summary": "Instant Meeting",
        "description": "Auto-created meeting",
        "start": {"dateTime": start_time.isoformat() + "Z", "timeZone": "UTC"},
        "end": {"dateTime": end_time.isoformat() + "Z", "timeZone": "UTC"},
        "conferenceData": {
            "createRequest": {
                "requestId": str(start_time.timestamp()),
                "conferenceSolutionKey": {"type": "hangoutsMeet"}
            }
        }
    }

    response = requests.post(event_url, headers=headers, json=data, params={"conferenceDataVersion": 1})
    result = response.json()
    logger.info(
        "Instant Google Meet creation finished for user %s. Calendar event id: %s. Meet link created: %s.",
        current_user.id,
        result.get("id"),
        bool(result.get("hangoutLink")),
    )

    return {"meet_link": result.get("hangoutLink"), "full_response": result}


# ─── Add Meeting ──────────────────────────────────────────────────────────────
@router.post("/add-meeting")
def add_meeting(
    payload: MeetingCreate,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    try:
        logger.info("Manual meeting creation was requested by user %s for link %s.", current_user.id, payload.meet_link)
        scheduled_at = _normalize_schedule_datetime(payload.scheduled_at, "scheduled_at") if payload.scheduled_at else None
        platform = _normalize_meeting_platform(payload.meet_link, payload.platform)
        meeting = crud.create_meeting_record(
            db,
            user_id=current_user.id,
            meet_link=payload.meet_link,
            title=payload.title,
            scheduled_at=scheduled_at,
            platform=platform,
        )
        return {
            "success": True,
            "message": "Meeting stored successfully",
            "data": {
                "meeting_id": meeting.id,
                "meet_link": meeting.meet_link,
                "title": meeting.title,
                "platform": meeting.platform,
                "scheduled_at": meeting.scheduled_at.isoformat() if meeting.scheduled_at else None,
                "status": meeting.status,
            }
        }
    except Exception as e:
        logger.warning("Manual meeting creation failed for user %s. Error: %s.", current_user.id, e)
        return {"success": False, "message": str(e), "data": {}}


@router.post("/create-scheduled-meeting")
def create_scheduled_meeting(
    payload: ScheduledMeetingCreate,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    try:
        logger.info(
            "Scheduled meeting creation was requested by user %s. Title: %s. Auto schedule bot: %s.",
            current_user.id,
            payload.title,
            payload.auto_schedule_bot,
        )
        start_time = _normalize_schedule_datetime(payload.start_time, "start_time")
        end_time = _normalize_schedule_datetime(payload.end_time, "end_time")
        if end_time <= start_time:
            logger.warning("Scheduled meeting creation was stopped for user %s because end_time is not after start_time.", current_user.id)
            return {"success": False, "message": "end_time must be after start_time", "data": {}}

        access_token = current_user.access_token
        event_url = "https://www.googleapis.com/calendar/v3/calendars/primary/events"
        headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}
        request_id = str(int(datetime.now(timezone.utc).timestamp() * 1000))
        external_meeting_url = google_calendar_service.find_link_in_text(payload.description)

        event_payload = {
            "summary": payload.title,
            "description": payload.description or "Scheduled meeting with Recall AI bot",
            "start": {"dateTime": start_time.isoformat(), "timeZone": "UTC"},
            "end": {"dateTime": end_time.isoformat(), "timeZone": "UTC"},
        }

        if not external_meeting_url:
            event_payload["conferenceData"] = {
                "createRequest": {
                    "requestId": request_id,
                    "conferenceSolutionKey": {"type": "hangoutsMeet"}
                }
            }

        response = requests.post(
            event_url,
            headers=headers,
            json=event_payload,
            params={"conferenceDataVersion": 1},
            timeout=60,
        )
        response.raise_for_status()
        event_data = response.json()
        meet_link = external_meeting_url or event_data.get("hangoutLink")
        if not meet_link:
            logger.warning("Scheduled meeting creation finished without a supported meeting link for user %s.", current_user.id)
            return {
                "success": False,
                "message": "No supported meeting link was found. Add a Teams/Zoom/Google Meet link in description or let Google Meet be created.",
                "data": {"calendar_response": event_data},
            }

        platform = google_calendar_service.get_meeting_platform(meet_link)
        if not platform:
            logger.warning("Scheduled meeting creation was stopped for user %s because the meeting link is not supported.", current_user.id)
            return {"success": False, "message": "Unsupported meeting link", "data": {"calendar_response": event_data}}

        meeting = crud.create_meeting_record(
            db,
            user_id=current_user.id,
            meet_link=meet_link,
            title=payload.title,
            scheduled_at=start_time,
            platform=platform,
            calendar_event_id=event_data.get("id"),
        )

        if payload.auto_schedule_bot:
            bot_response = recall_service.create_bot(
                meet_link,
                metadata={"meeting_id": str(meeting.id), "user_id": str(current_user.id)},
                join_at=_format_recall_datetime(start_time),
            )
            meeting = crud.set_meeting_recall_bot(db, meeting.id, bot_response)

        logger.info(
            "Scheduled meeting creation finished for user %s. Meeting id: %s. Calendar event id: %s. Bot created: %s.",
            current_user.id,
            meeting.id,
            event_data.get("id"),
            bool(meeting.recall_bot_id),
        )

        return {
            "success": True,
            "message": "Scheduled meeting created successfully",
            "data": {
                "meeting_id": meeting.id,
                "title": meeting.title,
                "meet_link": meeting.meet_link,
                "platform": meeting.platform,
                "scheduled_at": meeting.scheduled_at.isoformat() if meeting.scheduled_at else None,
                "calendar_event_id": event_data.get("id"),
                "google_meet_created": bool(event_data.get("hangoutLink")),
                "recall_bot_id": meeting.recall_bot_id,
                "status": meeting.status,
                "bot_status": meeting.bot_status,
            }
        }
    except requests.RequestException as e:
        detail = e.response.text if e.response is not None else str(e)
        logger.warning("Scheduled meeting creation failed for user %s because an external API call failed. Error: %s.", current_user.id, detail)
        return {"success": False, "message": f"External API error: {detail}", "data": {}}
    except Exception as e:
        logger.warning("Scheduled meeting creation failed for user %s. Error: %s.", current_user.id, e)
        return {"success": False, "message": str(e), "data": {}}


@router.post("/sync-calendar-scheduled-meetings")
def sync_calendar_scheduled_meetings(
    days_ahead: int = Query(default=7, ge=1, le=30),
    auto_schedule_bot: bool = Query(default=True),
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user),
):
    try:
        logger.info(
            "Calendar sync was requested by user %s. Days ahead: %s. Auto schedule bot: %s.",
            current_user.id,
            days_ahead,
            auto_schedule_bot,
        )
        if not current_user.calendar_sync_enabled:
            logger.info("Calendar sync request was stopped for user %s because calendar sync is disabled.", current_user.id)
            return {"success": False, "message": "Calendar sync is disabled for this user", "data": {}}

        result = google_calendar_service.sync_google_calendar(
            db=db,
            user=current_user,
            days_ahead=days_ahead,
            auto_schedule_bot=auto_schedule_bot,
            full_sync=True,
        )

        logger.info(
            "Calendar sync finished for user %s. Synced: %s. Skipped: %s. Deleted: %s.",
            current_user.id,
            len(result["synced"]),
            len(result["skipped"]),
            len(result["deleted"]),
        )

        return {
            "success": True,
            "message": "Calendar events synced successfully",
            "data": {
                "days_ahead": days_ahead,
                "synced_count": len(result["synced"]),
                "skipped_count": len(result["skipped"]),
                "deleted_count": len(result["deleted"]),
                "synced": result["synced"],
                "skipped": result["skipped"],
                "deleted": result["deleted"],
                "sync_token": result["sync_token"],
            },
        }
    except requests.RequestException as e:
        detail = e.response.text if e.response is not None else str(e)
        logger.warning("Calendar sync failed for user %s because the Google Calendar API returned an error. Error: %s.", current_user.id, detail)
        return {"success": False, "message": f"Google Calendar API error: {detail}", "data": {}}
    except Exception as e:
        logger.warning("Calendar sync failed for user %s. Error: %s.", current_user.id, e)
        return {"success": False, "message": str(e), "data": {}}


@router.post("/calendar/sync-setting")
def update_calendar_sync_setting(
    payload: CalendarSyncSettingRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user),
):
    try:
        logger.info(
            "Calendar sync setting update was requested by user %s. New value: %s.",
            current_user.id,
            payload.enabled,
        )
        user = crud.set_calendar_sync_enabled(db, current_user.id, payload.enabled)
        if not user:
            logger.warning("Calendar sync setting update failed because user %s was not found.", current_user.id)
            return {"success": False, "message": "User not found", "data": {}}

        if user.calendar_sync_enabled:
            start_calendar_watch_if_enabled(db, user)

        logger.info("Calendar sync setting was updated for user %s. Enabled: %s.", current_user.id, user.calendar_sync_enabled)

        return {
            "success": True,
            "message": "Calendar sync setting updated successfully",
            "data": {
                "calendar_sync_enabled": user.calendar_sync_enabled,
            },
        }
    except requests.RequestException as e:
        detail = e.response.text if e.response is not None else str(e)
        if (
            e.response is not None
            and e.response.status_code == 403
            and "ACCESS_TOKEN_SCOPE_INSUFFICIENT" in detail
        ):
            crud.set_calendar_sync_enabled(db, current_user.id, False)
            logger.warning("Calendar sync was disabled for user %s because Google Calendar permission is missing.", current_user.id)
            return {
                "success": False,
                "message": "Google Calendar permission is missing. Please sign in again and allow calendar access.",
                "data": {
                    "code": "google_calendar_scope_missing",
                    "requires_relogin": True,
                    "calendar_sync_enabled": False,
                },
            }
        logger.warning("Calendar sync setting update failed for user %s because the Google Calendar API returned an error. Error: %s.", current_user.id, detail)
        return {"success": False, "message": f"Google Calendar API error: {detail}", "data": {}}
    except Exception as e:
        logger.warning("Calendar sync setting update failed for user %s. Error: %s.", current_user.id, e)
        return {"success": False, "message": str(e), "data": {}}


@router.get("/calendar/sync-setting-status")
def get_calendar_sync_setting(
    current_user: User = Depends(get_approved_user),
):
    try:
        logger.info("Calendar sync setting status was requested by user %s.", current_user.id)
        return {
            "success": True,
            "message": "Setting fetched successfully",
            "data": {
                "calendar_sync_enabled": current_user.calendar_sync_enabled,
            },
        }
    except Exception as e:
        logger.warning("Calendar sync setting status lookup failed for user %s. Error: %s.", current_user.id, e)
        return {"success": False, "message": str(e), "data": {}}


@router.post("/calendar/watch/start")
def start_calendar_watch(
    payload: CalendarWatchRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user),
):
    try:
        logger.info(
            "Manual Google Calendar watch start was requested by user %s. Webhook URL: %s. Days ahead: %s. Auto schedule bot: %s.",
            current_user.id,
            payload.webhook_url,
            payload.days_ahead,
            payload.auto_schedule_bot,
        )
        if not current_user.calendar_sync_enabled:
            logger.info("Manual Google Calendar watch start was stopped for user %s because calendar sync is disabled.", current_user.id)
            return {"success": False, "message": "Calendar sync is disabled for this user", "data": {}}

        result = google_calendar_service.start_google_watch(
            db=db,
            user=current_user,
            webhook_url=payload.webhook_url,
            days_ahead=payload.days_ahead,
            auto_schedule_bot=payload.auto_schedule_bot,
        )
        watch_state = result["watch_state"]
        sync_result = result["sync_result"]
        logger.info(
            "Manual Google Calendar watch start finished for user %s. Channel id: %s. Synced: %s. Skipped: %s. Deleted: %s.",
            current_user.id,
            watch_state.channel_id if watch_state else None,
            len(sync_result["synced"]),
            len(sync_result["skipped"]),
            len(sync_result["deleted"]),
        )
        return {
            "success": True,
            "message": "Google Calendar watch started successfully",
            "data": {
                "channel_id": watch_state.channel_id,
                "calendar_id": watch_state.calendar_id,
                "expires_at": watch_state.expires_at.isoformat() if watch_state.expires_at else None,
                "sync_token": watch_state.sync_token,
                "synced_count": len(sync_result["synced"]),
                "skipped_count": len(sync_result["skipped"]),
                "deleted_count": len(sync_result["deleted"]),
            },
        }
    except requests.RequestException as e:
        detail = e.response.text if e.response is not None else str(e)
        logger.warning("Manual Google Calendar watch start failed for user %s because the Google Calendar API returned an error. Error: %s.", current_user.id, detail)
        return {"success": False, "message": f"Google Calendar API error: {detail}", "data": {}}
    except Exception as e:
        logger.warning("Manual Google Calendar watch start failed for user %s. Error: %s.", current_user.id, e)
        return {"success": False, "message": str(e), "data": {}}


@router.post("/webhook/google-calendar")
async def google_calendar_webhook(
    request: Request,
    background_tasks: BackgroundTasks,
):
    channel_id = request.headers.get("X-Goog-Channel-Id")
    channel_token = request.headers.get("X-Goog-Channel-Token")
    resource_state = request.headers.get("X-Goog-Resource-State")
    message_number = request.headers.get("X-Goog-Message-Number")

    if not channel_id:
        raise HTTPException(status_code=400, detail="Missing Google channel ID header")

    logger.info(
        "Google Calendar webhook request was accepted. Channel id: %s. Resource state: %s. Message number: %s.",
        channel_id,
        resource_state,
        message_number,
    )

    background_tasks.add_task(
        process_google_webhook,
        channel_id,
        channel_token,
        resource_state,
        message_number,
    )
    return {
        "success": True,
        "message": "Google Calendar webhook accepted",
        "data": {
            "channel_id": channel_id,
            "resource_state": resource_state,
        },
    }


# ─── Start Bot ────────────────────────────────────────────────────────────────
@router.post("/start-bot/{meeting_id}")
def start_bot(
    meeting_id: int,
    background_tasks: BackgroundTasks,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    try:
        logger.info("Recall bot creation was requested for meeting %s by user %s.", meeting_id, current_user.id)
        meeting = db.query(Meeting).filter(
            Meeting.id == meeting_id,
            Meeting.user_id == current_user.id
        ).first()
        if not meeting:
            logger.warning("Recall bot creation was stopped because meeting %s was not found for user %s.", meeting_id, current_user.id)
            return {"success": False, "message": "Meeting not found", "data": {}}

        join_at = None
        if meeting.scheduled_at and meeting.scheduled_at > datetime.now(timezone.utc):
            join_at = _format_recall_datetime(meeting.scheduled_at)

        bot_response = recall_service.create_bot(
            meeting.meet_link,
            metadata={
                "meeting_id": str(meeting.id),
                "user_id": str(current_user.id),
            },
            join_at=join_at,
        )
        meeting = crud.set_meeting_recall_bot(db, meeting.id, bot_response)
        logger.info(
            "Recall bot has been created for meeting %s. Bot id: %s. Meeting status: %s. Bot status: %s. Join time: %s.",
            meeting.id,
            meeting.recall_bot_id,
            meeting.status,
            meeting.bot_status,
            meeting.recall_join_at,
        )


        return {
            "success": True,
            "message": "Recall AI bot creation requested successfully",
            "data": {
                "meeting_id": meeting.id,
                "recall_bot_id": meeting.recall_bot_id,
                "status": meeting.status,
                "bot_status": meeting.bot_status,
                "provider": meeting.provider,
                "platform": meeting.platform,
                "scheduled_at": meeting.scheduled_at.isoformat() if meeting.scheduled_at else None,
                "recall_join_at": meeting.recall_join_at.isoformat() if meeting.recall_join_at else None,
            }
        }
    except requests.RequestException as e:
        detail = e.response.text if e.response is not None else str(e)
        return {"success": False, "message": f"Recall AI error: {detail}", "data": {}}
    except Exception as e:
        return {"success": False, "message": str(e), "data": {}}


# ─── Stop Recording ───────────────────────────────────────────────────────────
@router.post("/stop-recording/{meeting_id}")
def stop_recording(
    meeting_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    try:
        logger.info("Recall bot stop request was received for meeting %s from user %s.", meeting_id, current_user.id)
        meeting = db.query(Meeting).filter(
            Meeting.id == meeting_id,
            Meeting.user_id == current_user.id
        ).first()

        if not meeting:
            logger.warning("Recall bot stop request was stopped because meeting %s was not found for user %s.", meeting_id, current_user.id)
            return {"success": False, "message": "Meeting not found or not owned by you", "data": {}}

        if not meeting.recall_bot_id:
            logger.warning("Recall bot stop request was stopped because meeting %s does not have a Recall bot id.", meeting_id)
            return {"success": False, "message": "Recall AI bot has not been started for this meeting", "data": {}}

        stop_result = recall_service.stop_bot(meeting.recall_bot_id)
        logger.info(
            "Recall AI bot stop was requested for meeting %s. Bot id: %s. Method: %s. Previous bot status: %s.",
            meeting.id,
            meeting.recall_bot_id,
            stop_result.get("method"),
            stop_result.get("bot_status"),
        )
        recording = None
        if stop_result.get("method") in {"leave_call", "already_terminal"}:
            bot_details = recall_service.get_bot(meeting.recall_bot_id)
            crud.sync_meeting_from_recall_bot(db, meeting, bot_details)
            latest_recording = recall_service.latest_recording(bot_details)
            recording = crud.upsert_recording_from_recall(db, meeting.id, latest_recording)
            logger.info(
                "Meeting %s was refreshed after the stop request. Meeting status: %s. Bot status: %s. Recording id: %s. Recording status: %s.",
                meeting.id,
                meeting.status,
                meeting.bot_status,
                recording.recall_recording_id if recording else None,
                recording.status if recording else None,
            )
        else:
            logger.info(
                "Meeting %s stop request finished without live bot refresh because the bot was removed before joining. Method: %s.",
                meeting.id,
                stop_result.get("method"),
            )

        return {
            "success": True,
            "message": "Recall AI bot leave request sent successfully. Webhooks will handle recording and transcript lifecycle.",
            "data": {
                "meeting_id": meeting_id,
                "recall_bot_id": meeting.recall_bot_id,
                "status": meeting.status,
                "bot_status": meeting.bot_status,
                "recall_recording_id": recording.recall_recording_id if recording else None,
            }
        }
    except requests.RequestException as e:
        detail = e.response.text if e.response is not None else str(e)
        return {"success": False, "message": f"Recall AI error: {detail}", "data": {}}
    except Exception as e:
        return {"success": False, "message": f"Internal error: {str(e)}", "data": {}}


# ─── Recording Status ─────────────────────────────────────────────────────────
@router.get("/recording-status/{meeting_id}", response_model=RecordingResponse)
def get_recording_status(
    meeting_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    logger.info("Recording status was requested for meeting %s by user %s.", meeting_id, current_user.id)
    meeting = _get_meeting_for_read_access(db, meeting_id, current_user)
    if not meeting:
        raise HTTPException(status_code=404, detail="Meeting not found")

    if meeting.recall_bot_id:
        try:
            bot_details = recall_service.get_bot(meeting.recall_bot_id)
            crud.sync_meeting_from_recall_bot(db, meeting, bot_details)
            recording = crud.upsert_recording_from_recall(db, meeting.id, recall_service.latest_recording(bot_details))
            logger.info(
                "Meeting %s was refreshed while checking recording status. Meeting status: %s. Bot status: %s. Recording id: %s. Recording status: %s.",
                meeting.id,
                meeting.status,
                meeting.bot_status,
                recording.recall_recording_id if recording else None,
                recording.status if recording else None,
            )
        except requests.RequestException:
            pass

    recording = _refresh_recording_download_links_if_needed(db, meeting)
    if not recording:
        raise HTTPException(status_code=404, detail="No recording found for this meeting")
    logger.info(
        "Recording status response is ready for meeting %s. Recording id: %s. Recording status: %s.",
        meeting_id,
        recording.recall_recording_id,
        recording.status,
    )
    return recording



# ─── Shared Meeting List Helpers ──────────────────────────────────────────────

ACTIVE_STATUSES = {
    "pending",
    "live",
    "processing",
    "bot.joining_call",
    "bot.in_waiting_room",
    "bot.in_call_not_recording",
    "bot.in_call_recording",
    "recording.done",
    "transcript.processing",
}
FAILED_STATUSES = {
    "abort",
    "not_admitted",
    "too_short",
    "bot.fatal",
    "recording.failed",
    "transcript.failed",
}
COMPLETED_STATUSES = {
    "completed",
    "bot.call_ended",
    "bot.done",
    "transcript.done",
}
SCHEDULED_STATUSES = {"scheduled"}
STATUS_FILTER_GROUPS = {
    "active": ACTIVE_STATUSES,
    "failed": FAILED_STATUSES,
    "completed": COMPLETED_STATUSES,
    "scheduled": SCHEDULED_STATUSES,
}

def expand_status_filters(status_value: str | None) -> tuple[list[str], list[str]]:
    if not status_value:
        return [], []
    expanded_statuses: list[str] = []
    requested_tokens: list[str] = []
    for raw_value in status_value.split(","):
        normalized_value = raw_value.strip()
        if not normalized_value:
            continue
        requested_tokens.append(normalized_value.lower())
        grouped_statuses = STATUS_FILTER_GROUPS.get(normalized_value.lower())
        if grouped_statuses:
            expanded_statuses.extend(sorted(grouped_statuses))
        else:
            expanded_statuses.append(normalized_value)
    return list(dict.fromkeys(expanded_statuses)), list(dict.fromkeys(requested_tokens))

def get_status_segment(meeting_status: str | None) -> str:
    if meeting_status in ACTIVE_STATUSES:
        return "active"
    if meeting_status in FAILED_STATUSES:
        return "failed"
    if meeting_status in COMPLETED_STATUSES:
        return "completed"
    if meeting_status in SCHEDULED_STATUSES:
        return "scheduled"
    return "other"

def extract_participant_names(transcript: Transcript | None) -> list[str]:
    if not transcript or not transcript.participants:
        return []
    try:
        import json
        parsed_participants = json.loads(transcript.participants)
        if isinstance(parsed_participants, list):
            return [str(name) for name in parsed_participants]
    except Exception:
        pass
    return []

def matches_search_value(
    search_value: str | None,
    *,
    title: str | None,
    participant_names: list[str],
    organizer_email: str | None = None,
) -> bool:
    if not search_value:
        return True
    normalized_search = search_value.strip().lower()
    if not normalized_search:
        return True
    haystacks = [title or "", organizer_email or ""] + participant_names
    return any(normalized_search in value.lower() for value in haystacks if value)


# ─── List All Meetings ────────────────────────────────────────────────────────
@router.get("/list-meetings")
def list_meetings(
        page: int = 1,
        limit: int = 12,
        status: str | None = None,
        search: str | None = None,
        platform: str | None = None,
        meeting_type: str | None = Query(None, alias="type"),
        time_filter: str | None = Query(None, alias="time"),
        sort_by: str | None = Query(None, alias="sort_by"),
        db: Session = Depends(get_db),
        current_user: User = Depends(get_approved_user)
):
    logger.info(
        "Meeting list was requested by user %s. Page: %s. Limit: %s. Status filter: %s. Search: %s. Platform: %s. Type: %s. Time: %s. Sort: %s.",
        current_user.id,
        page,
        limit,
        status,
        search,
        platform,
        meeting_type,
        time_filter,
        sort_by,
    )
    now_utc = datetime.now(timezone.utc)

    def _should_refresh_meeting_in_list(meeting: Meeting) -> bool:
        if not meeting.recall_bot_id:
            return False
        if meeting.status in ACTIVE_STATUSES:
            return True
        if meeting.status in SCHEDULED_STATUSES and meeting.recall_join_at:
            return meeting.recall_join_at <= now_utc + timedelta(hours=2)
        return False

    def _get_reference_datetime(item) -> datetime:
        if getattr(item, "scheduled_at", None):
            return item.scheduled_at
        if getattr(item, "recall_join_at", None):
            return item.recall_join_at
        created_at = getattr(item, "created_at", None)
        if isinstance(created_at, datetime):
            return created_at
        if isinstance(created_at, date):
            return datetime.combine(created_at, datetime.min.time(), tzinfo=timezone.utc)
        return now_utc

    query = (
        db.query(Meeting)
        .outerjoin(Recording)
        .filter(Meeting.user_id == current_user.id)
    )

    requested_statuses, requested_status_tokens = expand_status_filters(status)
    if requested_statuses:
        query = query.filter(Meeting.status.in_(requested_statuses))

    all_meetings = query.all()
    recurring_candidates = crud.list_visible_recurring_candidates(
        db=db,
        user_id=current_user.id,
        now_utc=now_utc,
        window_end=now_utc + timedelta(days=google_calendar_service.RECURRING_MEETING_WINDOW_DAYS),
    )
    visible_candidate_event_ids = {
        candidate.google_event_id
        for candidate in recurring_candidates
        if candidate.google_event_id and not candidate.linked_meeting_id
    }
    filtered_meetings = []
    for meeting in all_meetings:
        meeting_event_id = meeting.source_event_id or meeting.calendar_event_id
        should_hide_superseded_recurring_meeting = (
            meeting.is_recurring
            and meeting.status == "abort"
            and meeting.scheduled_at
            and meeting.scheduled_at > now_utc
            and meeting_event_id in visible_candidate_event_ids
        )
        if should_hide_superseded_recurring_meeting:
            continue
        filtered_meetings.append(meeting)

    meeting_ids = [meeting.id for meeting in filtered_meetings]
    recordings_by_meeting_id: dict[int, Recording] = {}
    transcripts_by_meeting_id: dict[int, Transcript] = {}
    if meeting_ids:
        recordings_by_meeting_id = {
            recording.meeting_id: recording
            for recording in db.query(Recording).filter(Recording.meeting_id.in_(meeting_ids)).all()
        }
        transcripts_by_meeting_id = {
            transcript.meeting_id: transcript
            for transcript in db.query(Transcript).filter(Transcript.meeting_id.in_(meeting_ids)).all()
        }
    if search:
        filtered_meetings = [
            meeting
            for meeting in filtered_meetings
            if matches_search_value(
                search,
                title=meeting.title,
                participant_names=extract_participant_names(transcripts_by_meeting_id.get(meeting.id)),
            )
        ]
        recurring_candidates = [
            candidate
            for candidate in recurring_candidates
            if matches_search_value(
                search,
                title=candidate.title,
                participant_names=[],
            )
        ]

    entries = [("meeting", meeting) for meeting in filtered_meetings] + [
        ("candidate", candidate) for candidate in recurring_candidates if not candidate.linked_meeting_id
    ]
    if requested_status_tokens:
        entries = [
            entry for entry in entries
            if (
                entry[0] == "meeting" and entry[1].status in requested_statuses
            ) or (
                entry[0] == "candidate"
                and (
                    "scheduled" in requested_status_tokens
                    or f"recurring.{entry[1].bot_permission_status}".lower() in requested_status_tokens
                )
            )
        ]
    filtered_listing = build_meeting_list(
        entries,
        page=page,
        limit=limit,
        max_limit=12,
        platform=platform,
        meeting_type=meeting_type,
        time_filter=time_filter,
        sort_by=sort_by,
        reference_datetime_getter=lambda entry: _get_reference_datetime(entry[1]),
        title_getter=lambda entry: getattr(entry[1], "title", None),
        duration_seconds_getter=lambda entry: (
            0
            if entry[0] == "candidate"
            else (
                transcripts_by_meeting_id.get(entry[1].id).duration_seconds
                if transcripts_by_meeting_id.get(entry[1].id) and transcripts_by_meeting_id.get(entry[1].id).duration_seconds
                else (
                    int(
                        (
                            recordings_by_meeting_id.get(entry[1].id).stopped_at
                            - recordings_by_meeting_id.get(entry[1].id).started_at
                        ).total_seconds()
                    )
                    if recordings_by_meeting_id.get(entry[1].id)
                    and recordings_by_meeting_id.get(entry[1].id).started_at
                    and recordings_by_meeting_id.get(entry[1].id).stopped_at
                    else 0
                )
            )
        ),
        platform_getter=lambda entry: getattr(entry[1], "platform", None),
        is_recurring_getter=lambda entry: entry[0] == "candidate" or bool(getattr(entry[1], "is_recurring", False)),
        now_utc=now_utc,
    )

    sorted_entries = filtered_listing.all_items
    total = filtered_listing.total
    page_entries = filtered_listing.items

    recorded_meetings_count = 0
    total_recorded_duration_seconds = 0
    segment_counts: dict[str, int] = {}
    grouped_status_counts: dict[str, int] = {}

    for entry_type, summary_item in sorted_entries:
        if entry_type == "candidate":
            segment_counts["scheduled"] = segment_counts.get("scheduled", 0) + 1
            grouped_status_counts["scheduled"] = grouped_status_counts.get("scheduled", 0) + 1
            continue

        summary_meeting = summary_item
        transcript = transcripts_by_meeting_id.get(summary_meeting.id)
        recording = recordings_by_meeting_id.get(summary_meeting.id)
        segment_name = get_status_segment(summary_meeting.status)

        segment_counts[segment_name] = segment_counts.get(segment_name, 0) + 1
        grouped_status_counts[segment_name] = grouped_status_counts.get(segment_name, 0) + 1

        transcript_duration_seconds = transcript.duration_seconds if transcript and transcript.duration_seconds else 0
        if not transcript_duration_seconds and recording and recording.started_at and recording.stopped_at:
            transcript_duration_seconds = int((recording.stopped_at - recording.started_at).total_seconds())

        if transcript_duration_seconds > 0:
            recorded_meetings_count += 1
            total_recorded_duration_seconds += transcript_duration_seconds

    response_data = []
    recurring_candidate_by_event_id = {
        candidate.google_event_id: candidate for candidate in recurring_candidates
    }

    for entry_type, item in page_entries:
        if entry_type == "candidate":
            candidate: RecurringMeetingCandidate = item
            response_data.append(
                {
                    "entry_type": "recurring_candidate",
                    "meeting_id": None,
                    "candidate_id": candidate.id,
                    "title": candidate.title,
                    "meet_link": candidate.meet_link,
                    "platform": candidate.platform,
                    "status": f"recurring.{candidate.bot_permission_status}",
                    "status_segment": "scheduled",
                    "created_at": candidate.created_at,
                    "scheduled_at": candidate.scheduled_at,
                    "recall_join_at": None,
                    "participants_details": {
                        "count": 0,
                        "names": [],
                    },
                    "recording_details": {
                        "started_at": None,
                        "stopped_at": None,
                        "duration_seconds": 0,
                        "duration_formatted": "0:00:00",
                    },
                    "host_rsvp_status": candidate.host_rsvp_status,
                    "app_toggle_enabled": candidate.app_toggle_enabled,
                    "is_recurring": True,
                    "dashboard_visible": candidate.dashboard_visible,
                }
            )
            continue

        m = item
        if _should_refresh_meeting_in_list(m):
            try:
                bot_details = recall_service.get_bot(m.recall_bot_id)
                crud.sync_meeting_from_recall_bot(db, m, bot_details)
                recall_webhooks.enforce_waiting_room_timeout(db, m, schedule_if_needed=True)
                refreshed_recording = crud.upsert_recording_from_recall(db, m.id, recall_service.latest_recording(bot_details))
                if refreshed_recording:
                    recordings_by_meeting_id[m.id] = refreshed_recording
                logger.info(
                    "Meeting %s was refreshed while listing meetings. Meeting status: %s. Bot status: %s. Join time: %s.",
                    m.id,
                    m.status,
                    m.bot_status,
                    m.recall_join_at,
                )
            except requests.RequestException as exc:
                logger.warning("Meeting %s could not be refreshed while listing meetings. Error: %s.", m.id, exc)

        # Check if a recording exists for this meeting
        recording = recordings_by_meeting_id.get(m.id)
        transcript = transcripts_by_meeting_id.get(m.id)

        duration_seconds = 0
        started_at = None
        stopped_at = None

        if recording and recording.started_at and recording.stopped_at:
            started_at = recording.started_at
            stopped_at = recording.stopped_at
            # Calculate duration
            duration = stopped_at - started_at
            duration_seconds = int(duration.total_seconds())

        if transcript and transcript.duration_seconds:
            duration_seconds = transcript.duration_seconds

        participant_names = extract_participant_names(transcript)

        status_segment = get_status_segment(m.status)
        linked_candidate = crud.get_recurring_candidate_by_linked_meeting_id(db, current_user.id, m.id)
        if not linked_candidate:
            linked_candidate = recurring_candidate_by_event_id.get(m.source_event_id or m.calendar_event_id)
        meeting_payload = {
            "entry_type": "meeting",
            "meeting_id": m.id,
            "candidate_id": linked_candidate.id if linked_candidate else None,
            "title": m.title,
            "meet_link": m.meet_link,
            "platform": m.platform,
            "status": m.status,
            "status_segment": status_segment,
            "created_at": m.created_at,
            "scheduled_at": m.scheduled_at,
            "recall_join_at": m.recall_join_at,
            "host_rsvp_status": m.host_rsvp_status,
            "app_toggle_enabled": m.app_toggle_enabled,
            "is_recurring": m.is_recurring,
            "participants_details": {
                "count": len(participant_names),
                "names": participant_names,
            },
            "recording_details": {
                "started_at": started_at,
                "stopped_at": stopped_at,
                "duration_seconds": duration_seconds,
                "duration_formatted": str(timedelta(seconds=duration_seconds)) if duration_seconds else "0:00:00"
            }
        }

        response_data.append(meeting_payload)

    segments = [
        {
            "segment": segment_name,
            "total_count": segment_counts.get(segment_name, 0),
        }
        for segment_name in sorted(segment_counts.keys(), key=lambda name: STATUS_PRIORITY.get(name, STATUS_PRIORITY["other"]))
    ]

    return {
        "success": True,
        "pagination": filtered_listing.pagination,
        "summary": {
            "recorded_meetings_count": recorded_meetings_count,
            "total_recorded_duration_seconds": total_recorded_duration_seconds,
            "total_recorded_duration_formatted": str(timedelta(seconds=total_recorded_duration_seconds)) if total_recorded_duration_seconds else "0:00:00",
        },
        "segment_counts": segment_counts,
        "status_counts": grouped_status_counts,
        "data": response_data,
        "segments": segments,
    }


@router.get("/recurring-candidates/upcoming")
def list_upcoming_recurring_candidates(
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user),
):
    now_utc = datetime.now(timezone.utc)
    candidates = crud.list_visible_recurring_candidates(
        db=db,
        user_id=current_user.id,
        now_utc=now_utc,
        window_end=now_utc + timedelta(days=google_calendar_service.RECURRING_MEETING_WINDOW_DAYS),
    )
    logger.info(
        "Upcoming recurring candidates were requested by user %s. Candidates returned: %s.",
        current_user.id,
        len(candidates),
    )
    return {
        "success": True,
        "data": [
            {
                "candidate_id": candidate.id,
                "title": candidate.title,
                "meet_link": candidate.meet_link,
                "platform": candidate.platform,
                "scheduled_at": candidate.scheduled_at.isoformat() if candidate.scheduled_at else None,
                "host_rsvp_status": candidate.host_rsvp_status,
                "app_toggle_enabled": candidate.app_toggle_enabled,
                "bot_permission_status": candidate.bot_permission_status,
                "dashboard_visible": candidate.dashboard_visible,
                "linked_meeting_id": candidate.linked_meeting_id,
            }
            for candidate in candidates
        ],
    }


@router.get("/invited-meetings")
def list_invited_meetings(
    page: int = 1,
    limit: int = 12,
    status: str | None = None,
    search: str | None = None,
    platform: str | None = None,
    meeting_type: str | None = Query(None, alias="type"),
    time_filter: str | None = Query(None, alias="time"),
    sort_by: str | None = Query(None, alias="sort_by"),
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user),
):
    logger.info(
        "Invited meeting list was requested by user %s. Page: %s. Limit: %s. Status filter: %s. Search: %s. Platform: %s. Type: %s. Time: %s. Sort: %s.",
        current_user.id,
        page,
        limit,
        status,
        search,
        platform,
        meeting_type,
        time_filter,
        sort_by,
    )
    now_utc = datetime.now(timezone.utc)

    def _get_reference_datetime(item) -> datetime:
        invite = item["invite"]
        if getattr(invite, "scheduled_at", None):
            return invite.scheduled_at
        created_at = getattr(invite, "created_at", None)
        if isinstance(created_at, datetime):
            return created_at
        if isinstance(created_at, date):
            return datetime.combine(created_at, datetime.min.time(), tzinfo=timezone.utc)
        return now_utc

    recurring_invites = crud.list_visible_recurring_meeting_invites(
        db=db,
        guest_user_id=current_user.id,
        now_utc=now_utc,
        window_end=now_utc + timedelta(days=google_calendar_service.RECURRING_MEETING_WINDOW_DAYS),
    )
    non_recurring_invites = crud.list_visible_non_recurring_meeting_invites(
        db=db,
        guest_user_id=current_user.id,
        now_utc=now_utc,
        window_end=now_utc + timedelta(days=google_calendar_service.DEFAULT_SYNC_DAYS),
    )
    all_invites = [("recurring", i) for i in recurring_invites] + [("non_recurring", i) for i in non_recurring_invites]
    
    meeting_ids_from_invites = {inv.meeting_id for _, inv in all_invites if inv.meeting_id}
    google_event_ids = {inv.google_event_id for _, inv in all_invites if inv.google_event_id and not inv.meeting_id}
    
    meetings = []
    if meeting_ids_from_invites or google_event_ids:
        meetings = db.query(Meeting).filter(
            Meeting.id.in_(meeting_ids_from_invites) |
            Meeting.calendar_event_id.in_(google_event_ids) |
            Meeting.source_event_id.in_(google_event_ids)
        ).all()
        
    meeting_by_id = {m.id: m for m in meetings}
    meeting_by_event = {(m.user_id, m.calendar_event_id): m for m in meetings if m.calendar_event_id}
    meeting_by_source = {(m.user_id, m.source_event_id): m for m in meetings if m.source_event_id}


    def _should_refresh_meeting_in_list(meeting: Meeting) -> bool:
        if not meeting.recall_bot_id:
            return False
        if meeting.status in ACTIVE_STATUSES:
            return True
        if meeting.status in SCHEDULED_STATUSES and meeting.recall_join_at:
            return meeting.recall_join_at <= now_utc + timedelta(hours=2)
        return False

    def _resolve_meeting(invite, is_recurring):
        if invite.meeting_id and invite.meeting_id in meeting_by_id:
            return meeting_by_id[invite.meeting_id]
        if invite.google_event_id:
            m = meeting_by_event.get((invite.host_user_id, invite.google_event_id)) or \
                meeting_by_source.get((invite.host_user_id, invite.google_event_id))
            if m and (not is_recurring or m.is_recurring):
                return m
        return None

    meeting_ids = [m.id for m in meetings]
    recordings_by_meeting_id: dict[int, Recording] = {}
    transcripts_by_meeting_id: dict[int, Transcript] = {}
    if meeting_ids:
        recordings_by_meeting_id = {
            r.meeting_id: r for r in db.query(Recording).filter(Recording.meeting_id.in_(meeting_ids)).all()
        }
        transcripts_by_meeting_id = {
            t.meeting_id: t for t in db.query(Transcript).filter(Transcript.meeting_id.in_(meeting_ids)).all()
        }

    requested_statuses, requested_status_tokens = expand_status_filters(status)
    
    filtered_entries = []
    for inv_type, invite in all_invites:
        is_recurring = (inv_type == "recurring")
        meeting = _resolve_meeting(invite, is_recurring)

        if meeting and _should_refresh_meeting_in_list(meeting):
            try:
                bot_details = recall_service.get_bot(meeting.recall_bot_id)
                crud.sync_meeting_from_recall_bot(db, meeting, bot_details)
                recall_webhooks.enforce_waiting_room_timeout(db, meeting, schedule_if_needed=True)
                refreshed_recording = crud.upsert_recording_from_recall(db, meeting.id, recall_service.latest_recording(bot_details))
                if refreshed_recording:
                    recordings_by_meeting_id[meeting.id] = refreshed_recording
                logger.info(
                    "Meeting %s was refreshed while listing invited meetings. Meeting status: %s. Bot status: %s. Join time: %s.",
                    meeting.id,
                    meeting.status,
                    meeting.bot_status,
                    meeting.recall_join_at,
                )
            except Exception as e:
                logger.warning("Meeting %s could not be refreshed while listing invited meetings. Error: %s.", meeting.id, e)
        recording = recordings_by_meeting_id.get(meeting.id) if meeting else None
        transcript = transcripts_by_meeting_id.get(meeting.id) if meeting else None
        
        entry_status = meeting.status if meeting else "scheduled"
        if requested_statuses and entry_status not in requested_statuses:
            continue
            
        if search and not matches_search_value(
            search,
            title=invite.title,
            participant_names=extract_participant_names(transcript),
            organizer_email=invite.organizer_email,
        ):
            continue
            
        filtered_entries.append({
            "invite_type": inv_type,
            "invite": invite,
            "meeting": meeting,
            "recording": recording,
            "transcript": transcript,
        })
        
    filtered_listing = build_meeting_list(
        filtered_entries,
        page=page,
        limit=limit,
        max_limit=12,
        platform=platform,
        meeting_type=meeting_type,
        time_filter=time_filter,
        sort_by=sort_by,
        reference_datetime_getter=_get_reference_datetime,
        title_getter=lambda entry: getattr(entry["invite"], "title", None),
        duration_seconds_getter=lambda entry: (
            entry["transcript"].duration_seconds if entry["transcript"] and entry["transcript"].duration_seconds
            else (
                int((entry["recording"].stopped_at - entry["recording"].started_at).total_seconds())
                if entry["recording"] and entry["recording"].started_at and entry["recording"].stopped_at
                else 0
            )
        ),
        platform_getter=lambda entry: getattr(entry["invite"], "platform", None),
        is_recurring_getter=lambda entry: entry["invite_type"] == "recurring",
        now_utc=now_utc,
    )

    recorded_meetings_count = 0
    total_recorded_duration_seconds = 0
    segment_counts: dict[str, int] = {}
    grouped_status_counts: dict[str, int] = {}
    
    response_data = []

    for entry in filtered_listing.all_items:
        meeting = entry["meeting"]
        recording = entry["recording"]
        transcript = entry["transcript"]
        
        entry_status = meeting.status if meeting else "scheduled"
        segment_name = get_status_segment(entry_status)
        segment_counts[segment_name] = segment_counts.get(segment_name, 0) + 1
        grouped_status_counts[segment_name] = grouped_status_counts.get(segment_name, 0) + 1

        transcript_duration_seconds = transcript.duration_seconds if transcript and transcript.duration_seconds else 0
        if not transcript_duration_seconds and recording and recording.started_at and recording.stopped_at:
            transcript_duration_seconds = int((recording.stopped_at - recording.started_at).total_seconds())

        if transcript_duration_seconds > 0:
            recorded_meetings_count += 1
            total_recorded_duration_seconds += transcript_duration_seconds

    for entry in filtered_listing.items:
        invite = entry["invite"]
        meeting = entry["meeting"]
        recording = entry["recording"]
        transcript = entry["transcript"]
        
        response_data.append({
            "invite_id": invite.id,
            "invite_type": entry["invite_type"],
            "host_user_id": invite.host_user_id,
            "recurring_series_id": getattr(invite, "recurring_series_id", None),
            "google_event_id": invite.google_event_id,
            "title": invite.title,
            "meet_link": invite.meet_link,
            "platform": invite.platform,
            "scheduled_at": invite.scheduled_at.isoformat() if invite.scheduled_at else None,
            "guest_rsvp_status": getattr(invite, "guest_rsvp_status", None),
            "status": getattr(invite, "guest_rsvp_status", None),
            "organizer_email": invite.organizer_email,
            "is_recurring": entry["invite_type"] == "recurring",
            "meeting_id": meeting.id if meeting else None,
            "meeting_status": meeting.status if meeting else None,
            "has_meeting_access": bool(meeting),
            "recording_status": recording.status if recording else None,
            "transcript_status": transcript.status if transcript else None,
            "has_recording": bool(recording),
            "has_transcript": bool(transcript),
        })
        
    segments = [
        {
            "segment": segment_name,
            "total_count": segment_counts.get(segment_name, 0),
        }
        for segment_name in sorted(segment_counts.keys(), key=lambda name: STATUS_PRIORITY.get(name, STATUS_PRIORITY["other"]))
    ]

    return {
        "success": True,
        "pagination": filtered_listing.pagination,
        "summary": {
            "recorded_meetings_count": recorded_meetings_count,
            "total_recorded_duration_seconds": total_recorded_duration_seconds,
            "total_recorded_duration_formatted": str(timedelta(seconds=total_recorded_duration_seconds)) if total_recorded_duration_seconds else "0:00:00",
        },
        "segment_counts": segment_counts,
        "status_counts": grouped_status_counts,
        "data": response_data,
        "segments": segments,
    }


@router.post("/recurring-candidates/{candidate_id}/decision")
def decide_recurring_candidate(
    candidate_id: int,
    payload: RecurringCandidateDecisionRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user),
):
    action = (payload.action or "").strip().lower()
    candidate = db.query(RecurringMeetingCandidate).filter(
        RecurringMeetingCandidate.id == candidate_id,
        RecurringMeetingCandidate.user_id == current_user.id,
    ).first()
    if not candidate:
        return {"success": False, "message": "Recurring candidate not found", "data": {}}

    logger.info(
        "Recurring candidate decision was requested by user %s for candidate %s. Action: %s.",
        current_user.id,
        candidate.id,
        action,
    )

    if action in {"confirm", "on", "toggle_on"}:
        candidate = crud.update_recurring_candidate_toggle(db, candidate.id, True)
        decision = google_calendar_service.ensure_recurring_candidate_bot_state(
            db=db,
            user=current_user,
            candidate=candidate,
            auto_schedule_bot=True,
        )
        refreshed_candidate = crud.get_recurring_candidate_by_event_id(db, current_user.id, candidate.google_event_id)
        meeting_id = decision.get("meeting_id") or (refreshed_candidate.linked_meeting_id if refreshed_candidate else None)
        return {
            "success": True,
            "message": "Recurring meeting toggle turned on successfully",
            "data": {
                "candidate_id": candidate.id,
                "meeting_id": meeting_id,
                "host_rsvp_status": refreshed_candidate.host_rsvp_status if refreshed_candidate else candidate.host_rsvp_status,
                "app_toggle_enabled": True,
                "bot_permission_status": refreshed_candidate.bot_permission_status if refreshed_candidate else None,
                "bot_created": decision.get("bot_created", False),
                "recall_bot_id": decision.get("recall_bot_id"),
                "bot_join_mode": decision.get("bot_join_mode"),
                "reason": decision.get("reason"),
            },
        }

    if action in {"cancel", "off", "toggle_off"}:
        candidate = crud.update_recurring_candidate_toggle(db, candidate.id, False)
        linked_meeting = crud.get_meeting_by_id(db, candidate.linked_meeting_id) if candidate.linked_meeting_id else None
        if linked_meeting and linked_meeting.recall_bot_id:
            try:
                stop_result = recall_service.stop_bot(linked_meeting.recall_bot_id)
                logger.info(
                    "Recurring meeting toggle off stop was requested for bot %s and candidate %s. Method: %s. Previous bot status: %s.",
                    linked_meeting.recall_bot_id,
                    candidate.id,
                    stop_result.get("method"),
                    stop_result.get("bot_status"),
                )
            except requests.RequestException as exc:
                logger.warning(
                    "Recurring meeting toggle off could not stop Recall bot %s for candidate %s. Error: %s.",
                    linked_meeting.recall_bot_id,
                    candidate.id,
                    exc,
                )
        if linked_meeting:
            linked_meeting.app_toggle_enabled = False
            linked_meeting.recall_bot_id = None
            linked_meeting.bot_status = None
            linked_meeting.bot_payload = None
            linked_meeting.recall_join_at = None
            crud.mark_meeting_abort(db, linked_meeting.id)
            crud.unlink_recurring_candidate_meeting(db, candidate.id)
        refreshed_candidate = crud.set_recurring_candidate_status(
            db,
            candidate.id,
            "pending" if candidate.host_rsvp_status != "accepted" else "waiting_for_app_confirmation",
        )
        return {
            "success": True,
            "message": "Recurring meeting toggle turned off successfully",
            "data": {
                "candidate_id": candidate.id,
                "meeting_id": linked_meeting.id if linked_meeting else None,
                "status": linked_meeting.status if linked_meeting else None,
                "host_rsvp_status": refreshed_candidate.host_rsvp_status if refreshed_candidate else candidate.host_rsvp_status,
                "app_toggle_enabled": False,
                "bot_permission_status": refreshed_candidate.bot_permission_status if refreshed_candidate else None,
            },
        }

    return {"success": False, "message": "Unsupported action. Use on/off.", "data": {}}


def _normalize_schedule_datetime(value: datetime, field_name: str) -> datetime:
    if value.tzinfo is None:
        raise ValueError(f"{field_name} must include timezone information")
    return value.astimezone(timezone.utc)


def _format_recall_datetime(value: datetime) -> str:
    utc_value = value.astimezone(timezone.utc)
    return utc_value.isoformat().replace("+00:00", "Z")


SUPPORTED_MEETING_PLATFORMS = {"google_meet", "zoom", "microsoft_teams"}


def _normalize_meeting_platform(meeting_url: str, platform: str | None) -> str:
    inferred_platform = _infer_meeting_platform(meeting_url)
    if platform:
        normalized_platform = platform.strip().lower()
        if normalized_platform not in SUPPORTED_MEETING_PLATFORMS:
            raise ValueError(
                f"Unsupported platform '{platform}'. Use one of: google_meet, zoom, microsoft_teams"
            )
        if inferred_platform and normalized_platform != inferred_platform:
            raise ValueError(
                f"Platform '{normalized_platform}' does not match meeting URL. Expected '{inferred_platform}'."
            )
        return normalized_platform

    if inferred_platform:
        return inferred_platform

    raise ValueError(
        "Unable to infer meeting platform from URL. Pass platform as google_meet, zoom, or microsoft_teams."
    )


def _infer_meeting_platform(meeting_url: str) -> str | None:
    host = urlparse(meeting_url).netloc.lower()
    if host.startswith("www."):
        host = host[4:]

    if host == "meet.google.com":
        return "google_meet"
    if host.endswith("zoom.us"):
        return "zoom"
    if host in {"teams.microsoft.com", "teams.live.com"} or host.endswith(".teams.microsoft.com"):
        return "microsoft_teams"
    return None


def _extract_supported_meeting_url(text: str | None) -> str | None:
    if not text:
        return None

    candidates = re.findall(r"https?://[^\s<>()\"]+", text)
    for candidate in candidates:
        cleaned = candidate.rstrip(".,);]")
        if _infer_meeting_platform(cleaned):
            return cleaned
    return None


def _extract_calendar_event_meeting_url(event: dict) -> str | None:
    for candidate in [
        event.get("hangoutLink"),
        _extract_supported_meeting_url(event.get("location")),
        _extract_supported_meeting_url(event.get("description")),
    ]:
        if candidate:
            return candidate
    return None


def _extract_google_event_start(event: dict) -> datetime | None:
    start_data = event.get("start") or {}
    start_value = start_data.get("dateTime")
    if not start_value:
        return None
    parsed = crud.parse_iso_datetime(start_value)
    if not parsed:
        return None
    return parsed.astimezone(timezone.utc)


def _list_google_calendar_events(access_token: str, days_ahead: int) -> list[dict]:
    time_min = datetime.now(timezone.utc).isoformat()
    time_max = (datetime.now(timezone.utc) + timedelta(days=days_ahead)).isoformat()
    event_url = "https://www.googleapis.com/calendar/v3/calendars/primary/events"
    headers = {"Authorization": f"Bearer {access_token}"}

    events = []
    page_token = None
    while True:
        params = {
            "timeMin": time_min,
            "timeMax": time_max,
            "singleEvents": True,
            "orderBy": "startTime",
            "maxResults": 250,
        }
        if page_token:
            params["pageToken"] = page_token

        response = requests.get(event_url, headers=headers, params=params, timeout=60)
        response.raise_for_status()
        payload = response.json()
        events.extend(payload.get("items", []))
        page_token = payload.get("nextPageToken")
        if not page_token:
            return events


def _find_existing_scheduled_meeting(db: Session, user_id: int, meet_link: str, scheduled_at: datetime) -> Meeting | None:
    return db.query(Meeting).filter(
        Meeting.user_id == user_id,
        Meeting.meet_link == meet_link,
        Meeting.scheduled_at == scheduled_at,
    ).first()


def _extract_audio_download_url(recording: Recording | None) -> str | None:
    if not recording:
        return None
    if recording.audio_download_url:
        return recording.audio_download_url
    if not recording.recording_payload:
        return None
    try:
        payload = json.loads(recording.recording_payload)
    except json.JSONDecodeError:
        return None

    shortcuts = payload.get("media_shortcuts") or {}
    audio_mixed = shortcuts.get("audio_mixed") or {}
    return ((audio_mixed.get("data") or {}).get("download_url"))


def _is_recall_download_url(value: str | None) -> bool:
    if not value or not value.startswith(("http://", "https://")):
        return False
    host = (urlparse(value).hostname or "").lower()
    return "recall" in host


def _recording_links_need_refresh(recording: Recording | None) -> bool:
    if not recording:
        return False

    if not (
        recording.audio_download_url
        or recording.video_download_url
        or _extract_audio_download_url(recording)
        or recording.transcript_download_url
    ):
        return True

    storage_enabled = bool(
        os.getenv("AWS_ACCESS_KEY_ID")
        and os.getenv("AWS_SECRET_ACCESS_KEY")
        and os.getenv("AWS_S3_BUCKET")
    )
    if storage_enabled and (
        _is_recall_download_url(recording.audio_download_url)
        or _is_recall_download_url(recording.video_download_url)
        or _is_recall_download_url(_extract_audio_download_url(recording))
    ):
        return True

    return False


def _refresh_recording_download_links_if_needed(db: Session, meeting: Meeting) -> Recording | None:
    recording = crud.get_recording_by_meeting(db, meeting.id)
    if not recording:
        return None

    if not _recording_links_need_refresh(recording):
        return recording

    if not recording.recall_recording_id:
        return recording

    try:
        recording_detail = recall_service.get_recording(recording.recall_recording_id)
        refreshed_recording = crud.upsert_recording_from_recall(
            db,
            meeting.id,
            recording_detail,
            mark_links_refreshed=True,
        )
        logger.info(
            "Download links were refreshed for meeting %s. Recording id: %s.",
            meeting.id,
            recording.recall_recording_id,
        )
        return refreshed_recording
    except requests.RequestException as exc:
        logger.warning(
            "Download links could not be refreshed for meeting %s. Recording id: %s. Error: %s.",
            meeting.id,
            recording.recall_recording_id,
            exc,
        )
        return recording


def _get_meeting_for_read_access(db: Session, meeting_id: int, current_user: User) -> Meeting | None:
    meeting = db.query(Meeting).filter(
        Meeting.id == meeting_id,
        Meeting.user_id == current_user.id,
    ).first()
    if meeting:
        return meeting

    return crud.get_accessible_guest_meeting(db, current_user.id, meeting_id)



# ─── Transcript Status ────────────────────────────────────────────────────────
@router.get("/transcript-status/{meeting_id}")
def get_transcript_status(
    meeting_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    logger.info("Transcript status was requested for meeting %s by user %s.", meeting_id, current_user.id)
    meeting = _get_meeting_for_read_access(db, meeting_id, current_user)

    if not meeting:
        return {"success": False, "message": "Meeting not found", "data": {}}

    if meeting.recall_bot_id:
        try:
            bot_details = recall_service.get_bot(meeting.recall_bot_id)
            crud.sync_meeting_from_recall_bot(db, meeting, bot_details)
            recording = crud.upsert_recording_from_recall(db, meeting.id, recall_service.latest_recording(bot_details))
            logger.info(
                "Meeting %s was refreshed while checking transcript status. Meeting status: %s. Bot status: %s. Recording id: %s.",
                meeting.id,
                meeting.status,
                meeting.bot_status,
                recording.recall_recording_id if recording else None,
            )
        except requests.RequestException:
            pass

    transcript = crud.get_transcript_by_meeting(db, meeting_id)
    recording = _refresh_recording_download_links_if_needed(db, meeting)

    if transcript and transcript.recall_transcript_id:
        try:
            transcript_detail = recall_service.get_transcript(transcript.recall_transcript_id)
            transcript = crud.sync_recall_transcript_details(db, meeting.id, transcript_detail)
            if transcript.status == "done" and transcript.download_url and (
                not transcript.full_text or recall_service.transcript_content_needs_refresh(
                    transcript.transcript_json,
                    transcript.speaker_map,
                )
            ):
                logger.info("Transcript content download has started for meeting %s while checking transcript status.", meeting.id)
                normalized_payload = recall_service.normalize_transcript_payload(
                    recall_service.download_transcript_json(transcript.download_url)
                )
                transcript = crud.save_recall_transcript_content(db, meeting.id, normalized_payload)
            logger.info(
                "Transcript details were refreshed for meeting %s. Transcript id: %s. Status: %s.",
                meeting.id,
                transcript.recall_transcript_id,
                transcript.status,
            )
        except requests.RequestException:
            pass

    if not transcript:
        logger.info("Transcript status check found no transcript yet for meeting %s.", meeting_id)
        return {
            "success": False,
            "message": "Transcript not created yet. It will be requested automatically after recording.done, or you can call /create-transcript/{meeting_id} manually.",
            "data": {}
        }

    response_data = {
        "transcript_id": transcript.id,
        "meeting_id": meeting_id,
        "status": transcript.status,
        "language": transcript.language,
        "duration_seconds": transcript.duration_seconds,
        "error_message": transcript.error_message,
        "created_at": str(transcript.created_at),
        "completed_at": str(transcript.completed_at) if transcript.completed_at else None,
        "download_url": transcript.download_url,
        "provider_data_download_url": transcript.provider_data_download_url,
        "recording": None,
    }

    if recording:
        response_data["recording"] = {
            "recording_id": recording.id,
            "recall_recording_id": recording.recall_recording_id,
            "status": recording.status,
            "started_at": str(recording.started_at) if recording.started_at else None,
            "completed_at": str(recording.stopped_at) if recording.stopped_at else None,
            "audio_download_url": crud.resolve_storage_download_url(recording.audio_download_url or _extract_audio_download_url(recording)),
            "video_download_url": crud.resolve_storage_download_url(recording.video_download_url),
            "transcript_download_url": recording.transcript_download_url,
            "transcript_provider_data_download_url": recording.transcript_provider_data_download_url,
            "participant_events_download_url": recording.participant_events_download_url,
            "speaker_timeline_download_url": recording.speaker_timeline_download_url,
            "participants_download_url": recording.participants_download_url,
        }

    logger.info(
        "Transcript status response is ready for meeting %s. Local transcript id: %s. Recall transcript id: %s. Status: %s.",
        meeting_id,
        transcript.id,
        transcript.recall_transcript_id,
        transcript.status,
    )
    return {"success": True, "data": response_data}


@router.get("/transcript-view/{meeting_id}")
def get_transcript_view(
    meeting_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    logger.info("Transcript view was requested for meeting %s by user %s.", meeting_id, current_user.id)
    meeting = _get_meeting_for_read_access(db, meeting_id, current_user)

    if not meeting:
        return {"success": False, "message": "Meeting not found", "data": {}}

    transcript = crud.get_transcript_by_meeting(db, meeting_id)
    if not transcript:
        return {"success": False, "message": "Transcript not found", "data": {}}

    recording = _refresh_recording_download_links_if_needed(db, meeting)

    if transcript.status == "done" and transcript.download_url and (
        not transcript.full_text or recall_service.transcript_content_needs_refresh(
            transcript.transcript_json,
            transcript.speaker_map,
        )
    ):
        try:
            logger.info("Transcript content download has started for meeting %s while opening the transcript view.", meeting.id)
            normalized_payload = recall_service.normalize_transcript_payload(
                recall_service.download_transcript_json(transcript.download_url)
            )
            transcript = crud.save_recall_transcript_content(db, meeting.id, normalized_payload)
        except requests.RequestException as exc:
            return {"success": False, "message": f"Failed to download transcript content: {exc}", "data": {}}

    segments = json.loads(transcript.transcript_json) if transcript.transcript_json else []
    participants = json.loads(transcript.participants) if transcript.participants else []
    speaker_map = json.loads(transcript.speaker_map) if transcript.speaker_map else {}

    return {
        "success": True,
        "data": {
            "meeting_id": meeting.id,
            "transcript_id": transcript.id,
            "recall_transcript_id": transcript.recall_transcript_id,
            "status": transcript.status,
            "participants": participants,
            "speaker_map": speaker_map,
            "segments": segments,
            "full_text": transcript.full_text,
            "download_url": transcript.download_url,
            "provider_data_download_url": transcript.provider_data_download_url,
            "video_download_url": crud.resolve_storage_download_url(recording.video_download_url) if recording else None,
            "recording": {
                "audio_download_url": crud.resolve_storage_download_url(recording.audio_download_url) if recording else None,
                "video_download_url": crud.resolve_storage_download_url(recording.video_download_url) if recording else None,
                "transcript_download_url": recording.transcript_download_url if recording else None,
                "transcript_provider_data_download_url": recording.transcript_provider_data_download_url if recording else None,
            },
        }
    }


@router.get("/transcript-pdf/{meeting_id}")
def get_transcript_pdf(
    meeting_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    logger.info("Transcript PDF download was requested for meeting %s by user %s.", meeting_id, current_user.id)
    if current_user.role == "admin":
        meeting = db.query(Meeting).filter(Meeting.id == meeting_id).first()
    else:
        meeting = _get_meeting_for_read_access(db, meeting_id, current_user)

    if not meeting:
        logger.warning("Transcript PDF download was stopped because meeting %s was not found for user %s.", meeting_id, current_user.id)
        return {"success": False, "message": "Meeting not found", "data": {}}

    transcript = crud.get_transcript_by_meeting(db, meeting_id)
    if not transcript:
        logger.warning("Transcript PDF download was stopped because transcript data was not found for meeting %s.", meeting_id)
        return {"success": False, "message": "Transcript not found", "data": {}}

    recording = _refresh_recording_download_links_if_needed(db, meeting)

    if transcript.status == "done" and transcript.download_url and (
        not transcript.full_text or recall_service.transcript_content_needs_refresh(
            transcript.transcript_json,
            transcript.speaker_map,
        )
    ):
        try:
            normalized_payload = recall_service.normalize_transcript_payload(
                recall_service.download_transcript_json(transcript.download_url)
            )
            transcript = crud.save_recall_transcript_content(db, meeting.id, normalized_payload)
        except requests.RequestException as exc:
            logger.warning(
                "Transcript PDF refresh from Recall failed for meeting %s (Error: %s). Falling back to database transcript.",
                meeting.id,
                exc,
            )

    segments = json.loads(transcript.transcript_json) if transcript.transcript_json else []
    participants = json.loads(transcript.participants) if transcript.participants else []
    pdf_bytes = build_transcript_pdf(
        title=meeting.title or f"Meeting {meeting.id}",
        meeting_id=meeting.id,
        transcript=transcript,
        meeting=meeting,
        recording=recording,
        participants=participants,
        segments=segments,
    )
    transcript_date = (
        meeting.scheduled_at
        or (recording.started_at if recording else None)
        or transcript.completed_at
        or transcript.created_at
    )
    date_suffix = transcript_date.strftime("%Y-%m-%d") if transcript_date else "undated"
    base_title = re.sub(r"[^A-Za-z0-9._-]+", "_", (meeting.title or f"meeting_{meeting.id}")).strip("_") or f"meeting_{meeting.id}"
    filename = f"{base_title}_{date_suffix}"
    logger.info("Transcript PDF is ready for meeting %s. File name: %s.pdf.", meeting.id, filename)

    return StreamingResponse(
        BytesIO(pdf_bytes),
        media_type="application/pdf",
        headers={
            "Content-Disposition": f'attachment; filename="{filename}.pdf"'
        },
    )


@router.get("/recording-audio/{meeting_id}")
def get_recording_audio(
    meeting_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    logger.info("Recording audio download was requested for meeting %s by user %s.", meeting_id, current_user.id)
    meeting = _get_meeting_for_read_access(db, meeting_id, current_user)

    if not meeting:
        raise HTTPException(status_code=404, detail="Meeting not found")

    recording = _refresh_recording_download_links_if_needed(db, meeting)
    audio_download_url = _extract_audio_download_url(recording)
    target_url = crud.resolve_storage_download_url(audio_download_url or (recording.video_download_url if recording else None))
    if not recording or not target_url:
        raise HTTPException(status_code=404, detail="Recording not found")
    logger.info("Recording audio redirect is ready for meeting %s.", meeting_id)
    return RedirectResponse(target_url)


# ─── Correct Speaker Names ────────────────────────────────────────────────────
@router.post("/correct-speaker/{meeting_id}")
def correct_speaker(
    meeting_id: int,
    payload: dict,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    logger.info("Manual speaker correction was requested for meeting %s by user %s, but this feature is not supported.", meeting_id, current_user.id)
    return {
        "success": False,
        "message": "Manual speaker correction is not supported in this Recall AI integration yet",
        "data": {}
    }


@router.post("/transcribe-file")
async def transcribe_file(
    file: UploadFile = File(...),
    current_user: User = Depends(get_approved_user),
):
    logger.info("Direct file transcription was requested by user %s for file %s, but this feature is disabled in the current flow.", current_user.id, file.filename)
    return {
        "success": False,
        "message": "Direct file transcription is disabled in the Recall AI flow. Use meeting bot recording plus /create-transcript/{meeting_id}.",
        "data": {}
    }

@router.post("/meeting/abort/{meeting_id}")
def abort_meeting(
    meeting_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    try:
        logger.info("Meeting abort was requested for meeting %s by user %s.", meeting_id, current_user.id)
        existing_meeting = db.query(Meeting).filter(
            Meeting.id == meeting_id,
            Meeting.user_id == current_user.id
        ).first()

        if not existing_meeting:
            logger.warning("Meeting abort was stopped because meeting %s was not found for user %s.", meeting_id, current_user.id)
            return {
                "success": False,
                "message": "Meeting not found",
                "data": {}
            }

        meeting = crud.mark_meeting_abort(db, meeting_id)

        if not meeting:
            logger.warning("Meeting abort could not be completed because meeting %s was not found during update.", meeting_id)
            return {
                "success": False,
                "message": "Meeting not found",
                "data": {}
            }

        logger.info("Meeting %s was marked as aborted by user %s.", meeting.id, current_user.id)
        return {
            "success": True,
            "message": "Meeting marked as abort",
            "data": {
                "meeting_id": meeting.id,
                "status": meeting.status
            }
        }

    except Exception as e:
        logger.warning("Meeting abort failed for meeting %s and user %s. Error: %s.", meeting_id, current_user.id, e)
        return {
            "success": False,
            "message": str(e),
            "data": {}
        }


@router.post("/create-transcript/{meeting_id}")
def create_recall_transcript(
    meeting_id: int,
    language_code: str = "auto",
    db: Session = Depends(get_db),
    current_user: User = Depends(get_approved_user)
):
    try:
        logger.info(
            "Manual transcript creation was requested for meeting %s by user %s. Language: %s.",
            meeting_id,
            current_user.id,
            language_code,
        )
        meeting = db.query(Meeting).filter(
            Meeting.id == meeting_id,
            Meeting.user_id == current_user.id
        ).first()

        if not meeting:
            logger.warning("Manual transcript creation was stopped because meeting %s was not found for user %s.", meeting_id, current_user.id)
            return {"success": False, "message": "Meeting not found", "data": {}}

        if not meeting.recall_bot_id:
            logger.warning("Manual transcript creation was stopped because meeting %s does not have a Recall bot id.", meeting_id)
            return {"success": False, "message": "Recall AI bot has not been started for this meeting", "data": {}}

        bot_details = recall_service.get_bot(meeting.recall_bot_id)
        crud.sync_meeting_from_recall_bot(db, meeting, bot_details)
        latest_recording = recall_service.latest_recording(bot_details)
        recording = crud.upsert_recording_from_recall(db, meeting.id, latest_recording)

        if not recording or not recording.recall_recording_id:
            logger.warning("Manual transcript creation was stopped because recording data is not ready for meeting %s.", meeting_id)
            return {"success": False, "message": "Recall AI recording is not ready yet", "data": {}}

        transcript_response = recall_service.create_transcript(recording.recall_recording_id, language_code=language_code)
        transcript = crud.save_recall_transcript_request(db, meeting.id, transcript_response)
        logger.info(
            "Manual transcript creation was submitted for meeting %s. Recording id: %s. Transcript id: %s. Status: %s.",
            meeting.id,
            recording.recall_recording_id,
            transcript.recall_transcript_id,
            transcript.status,
        )

        return {
            "success": True,
            "message": "Recall AI transcript creation requested successfully",
            "data": {
                "meeting_id": meeting.id,
                "recording_id": recording.recall_recording_id,
                "transcript_id": transcript.recall_transcript_id,
                "status": transcript.status,
            }
        }
    except requests.RequestException as e:
        detail = e.response.text if e.response is not None else str(e)
        logger.warning("Manual transcript creation failed for meeting %s because Recall AI returned an error. Error: %s.", meeting_id, detail)
        return {"success": False, "message": f"Recall AI error: {detail}", "data": {}}
    except Exception as e:
        logger.warning("Manual transcript creation failed for meeting %s. Error: %s.", meeting_id, e)
        return {"success": False, "message": str(e), "data": {}}


@router.post("/webhook/recall")
async def recall_webhook(
    request: Request,
    background_tasks: BackgroundTasks,
    token: str | None = Query(default=None),
    db: Session = Depends(get_db),
):
    raw_body = (await request.body()).decode("utf-8")
    # print("[recall][webhook] request received")
    try:
        recall_webhooks.verify_recall_request(dict(request.headers), raw_body, token)
    except ValueError as exc:
        print(f"[recall][webhook] verification failed error={exc}")
        raise HTTPException(status_code=401, detail=str(exc))

    try:
        payload = json.loads(raw_body)
    except json.JSONDecodeError as exc:
        raise HTTPException(status_code=400, detail=f"Invalid JSON payload: {exc}")

    event_name = payload.get("event", "unknown")
    data = payload.get("data") or {}
    if event_name.startswith("bot."):
        bot_id = data.get("id")
    else:
        bot_id = (data.get("bot") or {}).get("id")

    if event_name.startswith("recording."):
        recording_id = data.get("id")
    else:
        recording_id = (data.get("recording") or {}).get("id")

    if event_name.startswith("transcript."):
        transcript_id = data.get("id")
    else:
        transcript_id = (data.get("transcript") or {}).get("id")

    if not bot_id and isinstance(data.get("bot"), dict):
        bot_id = data["bot"].get("id")
    if not recording_id and isinstance(data.get("recording"), dict):
        recording_id = data["recording"].get("id")
    if not transcript_id and isinstance(data.get("transcript"), dict):
        transcript_id = data["transcript"].get("id")

    status_data = data.get("status") or {}
    if event_name.startswith("bot.") and isinstance(data.get("data"), dict):
        status_data = data.get("data") or {}
    webhook_id = request.headers.get("webhook-id") or request.headers.get("svix-id")
    print(
        f"[recall][webhook] accepted webhook_id={webhook_id} event={event_name} "
        f"bot_id={bot_id} recording_id={recording_id} transcript_id={transcript_id}"
    )

    if not webhook_id:
        raise HTTPException(status_code=400, detail="Missing webhook ID header")

    webhook_event, created = crud.create_recall_webhook_event(
        db=db,
        webhook_id=webhook_id,
        event=event_name,
        payload=payload,
        bot_id=bot_id,
        recording_id=recording_id,
        transcript_id=transcript_id,
        status_code=(status_data.get("code") or event_name),
        status_sub_code=status_data.get("sub_code"),
    )

    if created:
        print(f"[recall][webhook] queued async processing webhook_id={webhook_id} event={event_name}")
        background_tasks.add_task(recall_webhooks.process_recall_webhook_event, webhook_event.id)
    else:
        print(f"[recall][webhook] duplicate ignored webhook_id={webhook_id} event={event_name}")

    return {
        "success": True,
        "message": "Webhook accepted",
        "data": {
            "webhook_id": webhook_id,
            "event": event_name,
            "processed_async": created,
        }
    }
