"""
Scheduling API - CRUD operations for scheduled jobs
"""

from fastapi import APIRouter, Depends, HTTPException
from typing import Dict, Any, Optional, Tuple
from pydantic import BaseModel
from datetime import datetime, timezone
import json
from .deps import require_api_key
from ...services.soql_preflight import preflight_soql_query
from ...services.scheduler_service import _records_to_sheet_table

router = APIRouter(prefix="/salesforce/schedules", tags=["Scheduling"])


class CreateScheduleRequest(BaseModel):
    name: str
    job_type: str  # 'soql_query' or 'report_export'
    config: Dict[str, Any]  # {soql, target_sheet} or {report_id, target_sheet}
    schedule: str  # 'daily', 'hourly', or cron expression
    sheet_id: str
    sheet_gid: Optional[str] = None


class UpdateScheduleRequest(BaseModel):
    name: Optional[str] = None
    job_type: Optional[str] = None  # 'soql_query' or 'report_export'
    config: Optional[Dict[str, Any]] = None
    schedule: Optional[str] = None  # 'daily', 'hourly', or cron expression
    sheet_gid: Optional[str] = None


async def _preflight_soql_query(gateway, soql: str) -> None:
    """Validate a SOQL query before persisting it to a scheduled job."""
    await preflight_soql_query(
        gateway,
        soql,
        user="schedule-preflight",
        ip="0.0.0.0",
        allow_no_limit=True,
    )


async def _normalize_schedule_config(
    *,
    job_type: str,
    config: Dict[str, Any],
    request_name: Optional[str],
    sheet_gid: Optional[str],
    gateway,
) -> Tuple[Dict[str, Any], Optional[str]]:
    normalized_job_type = str(job_type or "").strip()
    if normalized_job_type not in ("soql_query", "report_export"):
        raise HTTPException(
            400,
            "Invalid job_type. Supported values: soql_query, report_export.",
        )

    normalized_config = dict(config or {})
    if request_name is not None:
        name_value = str(request_name).strip()
        if not name_value:
            raise HTTPException(400, "name cannot be empty")
        normalized_config["name"] = name_value

    target_sheet = (
        normalized_config.get("target_sheet")
        or normalized_config.get("sheet_name")
        or normalized_config.get("name")
    )
    if not target_sheet:
        raise HTTPException(400, "target_sheet is required")
    normalized_config["target_sheet"] = str(target_sheet).strip()

    if normalized_job_type == "soql_query":
        soql = str(normalized_config.get("soql") or "").strip()
        if not soql:
            raise HTTPException(400, "SOQL query is required")
        normalized_config["soql"] = soql
        await _preflight_soql_query(gateway, soql)
    else:
        report_id = str(normalized_config.get("report_id") or "").strip()
        if not report_id:
            raise HTTPException(400, "Report ID is required")
        normalized_config["report_id"] = report_id

    sheet_gid_value = (
        sheet_gid
        or normalized_config.get("sheet_gid")
        or normalized_config.get("sheetGid")
    )
    if sheet_gid_value is not None:
        sheet_gid_value = str(sheet_gid_value).strip()
        if sheet_gid_value:
            normalized_config["sheet_gid"] = sheet_gid_value
        else:
            normalized_config.pop("sheet_gid", None)
            sheet_gid_value = None

    cached_title = normalized_config.get("cached_sheet_title")
    if not cached_title:
        cached_title = (
            normalized_config.get("target_sheet")
            or normalized_config.get("sheet_name")
            or normalized_config.get("name")
        )
    if cached_title:
        normalized_config["cached_sheet_title"] = str(cached_title).strip()

    return normalized_config, sheet_gid_value


@router.post("")
async def create_schedule(
    request: CreateScheduleRequest, api_ctx=Depends(require_api_key)
):
    """Create a new scheduled job."""
    from ...db import get_session
    from ...models import ScheduledJob
    from ...middleware.quota_checker import get_quota
    from ...services.google_sheets_oauth import ensure_google_access_token
    from ...services.salesforce_gateway import SalesforceGateway
    from croniter import croniter
    import uuid

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    schedule_name = str(request.name or "").strip()
    if not schedule_name:
        raise HTTPException(400, "name is required")

    async for db in get_session():
        try:
            access_token, _integration = await ensure_google_access_token(
                str(account_id), db
            )
            if not access_token:
                raise HTTPException(
                    403,
                    "Google Sheets connection required to create schedules. Connect Google in Schedule Manager first.",
                )

            # Check quota limits
            quota = await get_quota(str(account_id), db)
            limits = quota.get_limits()

            # Count active schedules
            from sqlalchemy import select, func

            result = await db.execute(
                select(func.count(ScheduledJob.id)).where(
                    ScheduledJob.account_id == str(account_id),
                    ScheduledJob.is_active == True,
                )
            )
            active_count = result.scalar() or 0

            if active_count >= limits["max_schedules"]:
                raise HTTPException(
                    429,
                    f"Schedule limit reached ({active_count}/{limits['max_schedules']}). Upgrade to Pro for more schedules.",
                )

            # Validate schedule format
            if request.schedule not in ["daily", "hourly"]:
                # Validate cron if not preset
                try:
                    croniter(request.schedule)
                except Exception:
                    raise HTTPException(
                        400,
                        "Invalid schedule format. Use 'daily', 'hourly', or valid cron expression.",
                    )

            # Check if frequency is allowed for tier
            if (
                request.schedule == "hourly"
                and "hourly" not in limits["allowed_frequencies"]
            ):
                raise HTTPException(403, "Hourly schedules require Pro tier")

            # Calculate next run time
            from ...services.scheduler_service import SchedulerService

            next_run = SchedulerService()._calculate_next_run(
                request.schedule, datetime.utcnow()
            )

            # Create scheduled job
            if request.config is not None and not isinstance(request.config, dict):
                raise HTTPException(400, "config must be an object")
            config = dict(request.config or {})
            gateway = await SalesforceGateway.for_tenant(str(account_id), db)
            config, sheet_gid = await _normalize_schedule_config(
                job_type=request.job_type,
                config=config,
                request_name=schedule_name,
                sheet_gid=request.sheet_gid,
                gateway=gateway,
            )

            job = ScheduledJob(
                id=f"job_{uuid.uuid4().hex[:12]}",
                account_id=str(account_id),
                sheet_id=request.sheet_id,
                sheet_gid=str(sheet_gid) if sheet_gid is not None else None,
                job_type=str(request.job_type).strip(),
                config=config,
                schedule=request.schedule,
                next_run_at=next_run,
                is_active=True,
                created_at=datetime.utcnow(),
                updated_at=datetime.utcnow(),
            )

            db.add(job)
            await db.commit()

            return {
                "ok": True,
                "data": {
                    "id": job.id,
                    "name": schedule_name,
                    "next_run_at": next_run.isoformat(),
                },
            }

        except HTTPException:
            raise
        except Exception as e:
            raise HTTPException(500, "Failed to create schedule")


@router.get("")
async def list_schedules(
    sheet_id: Optional[str] = None,
    api_ctx=Depends(require_api_key),
):
    """List scheduled jobs for current user (optionally filtered by sheet_id)."""
    from ...db import get_session
    from ...models import ScheduledJob
    from sqlalchemy import select

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    async for db in get_session():
        try:
            query = select(ScheduledJob).where(
                ScheduledJob.account_id == str(account_id)
            )
            if sheet_id:
                query = query.where(ScheduledJob.sheet_id == sheet_id)
            query = query.order_by(ScheduledJob.created_at.desc())
            result = await db.execute(query)

            jobs = result.scalars().all()

            return {
                "ok": True,
                "data": {
                    "schedules": [
                        {
                            "id": job.id,
                            "name": job.config.get("name")
                            if isinstance(job.config, dict)
                            else job.job_type,
                            "sheet_id": job.sheet_id,
                            "sheet_gid": job.sheet_gid,
                            "job_type": job.job_type,
                            "config": job.config,
                            "schedule": job.schedule,
                            "next_run_at": job.next_run_at.isoformat()
                            if job.next_run_at
                            else None,
                            "last_run_at": job.last_run_at.isoformat()
                            if job.last_run_at
                            else None,
                            "last_write_status": job.last_write_status,
                            "last_write_at": job.last_write_at.isoformat()
                            if job.last_write_at
                            else None,
                            "last_write_error": job.last_write_error,
                            "is_active": job.is_active,
                            "paused_by_system": job.paused_by_system,
                            "pause_reason": job.pause_reason,
                            "error_count": job.error_count,
                        }
                        for job in jobs
                    ]
                },
            }

        except Exception as e:
            raise HTTPException(500, "Failed to list schedules")


@router.get("/status")
async def get_scheduler_status(api_ctx=Depends(require_api_key)):
    """Return schedule worker runtime status for the current account."""
    from ...db import get_session
    from ...models import ScheduledJob
    from ...services.scheduler_service import get_scheduler
    from sqlalchemy import select, func, and_

    try:
        from ...models import SystemConfig
    except Exception:
        SystemConfig = None  # type: ignore[assignment]

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    now = datetime.utcnow()
    async for db in get_session():
        try:
            scheduler = await get_scheduler()
            overdue_result = await db.execute(
                select(func.count(ScheduledJob.id)).where(
                    and_(
                        ScheduledJob.account_id == str(account_id),
                        ScheduledJob.is_active == True,
                        ScheduledJob.paused_by_system == False,
                        ScheduledJob.next_run_at < now,
                    )
                )
            )
            overdue_jobs = int(overdue_result.scalar() or 0)

            leader_lock_alive = False
            leader_lock = None
            if SystemConfig is not None:
                leader_result = await db.execute(
                    select(SystemConfig).where(SystemConfig.key == "scheduler_leader")
                )
                leader_lock = leader_result.scalar_one_or_none()
                if leader_lock:
                    expires_at = leader_lock.expires_at
                    if expires_at is None:
                        leader_lock_alive = True
                    elif expires_at.tzinfo is not None:
                        leader_lock_alive = expires_at > datetime.now(timezone.utc)
                    else:
                        leader_lock_alive = expires_at > now

            return {
                "ok": True,
                "data": {
                    "running": bool(getattr(scheduler.scheduler, "running", False)),
                    "is_leader": bool(getattr(scheduler, "is_leader", False)),
                    "lock_mode": "db_leader_lock"
                    if bool(getattr(scheduler, "lock_acquired", False))
                    else "row_lock_only",
                    "leader_lock_alive": leader_lock_alive,
                    "leader_worker_id": leader_lock.value
                    if leader_lock_alive and leader_lock
                    else None,
                    "leader_lock_expires_at": leader_lock.expires_at.isoformat()
                    if leader_lock and leader_lock.expires_at
                    else None,
                    "overdue_jobs": overdue_jobs,
                    "checked_at": now.isoformat(),
                },
            }
        except Exception:
            raise HTTPException(500, "Failed to fetch scheduler status")


@router.patch("/{schedule_id}")
async def update_schedule(
    schedule_id: str,
    request: UpdateScheduleRequest,
    api_ctx=Depends(require_api_key),
):
    """Update schedule settings for an existing scheduled job."""
    from ...db import get_session
    from ...models import ScheduledJob
    from ...middleware.quota_checker import get_quota
    from ...services.scheduler_service import SchedulerService
    from ...services.salesforce_gateway import SalesforceGateway
    from sqlalchemy import select
    from croniter import croniter

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    async for db in get_session():
        try:
            result = await db.execute(
                select(ScheduledJob).where(
                    ScheduledJob.id == schedule_id,
                    ScheduledJob.account_id == str(account_id),
                )
            )
            job = result.scalar_one_or_none()
            if not job:
                raise HTTPException(404, "Schedule not found")

            incoming_schedule = (
                request.schedule.strip()
                if isinstance(request.schedule, str)
                else None
            )
            schedule_value = incoming_schedule or job.schedule
            if not schedule_value:
                raise HTTPException(400, "schedule is required")

            # Validate schedule format
            if schedule_value not in ["daily", "hourly"]:
                try:
                    croniter(schedule_value)
                except Exception:
                    raise HTTPException(
                        400,
                        "Invalid schedule format. Use 'daily', 'hourly', or valid cron expression.",
                    )

            # Check tier limits for hourly schedules
            if schedule_value == "hourly":
                quota = await get_quota(str(account_id), db)
                limits = quota.get_limits()
                if "hourly" not in limits["allowed_frequencies"]:
                    raise HTTPException(403, "Hourly schedules require Pro tier")

            existing_config = (
                json.loads(job.config)
                if isinstance(job.config, str)
                else dict(job.config or {})
            )
            if request.config is not None and not isinstance(request.config, dict):
                raise HTTPException(400, "config must be an object")

            config = dict(existing_config)
            if isinstance(request.config, dict):
                config.update(request.config)

            requested_job_type = (
                request.job_type.strip() if isinstance(request.job_type, str) else None
            )
            job_type = requested_job_type or job.job_type

            gateway = await SalesforceGateway.for_tenant(str(account_id), db)
            config, sheet_gid_value = await _normalize_schedule_config(
                job_type=job_type,
                config=config,
                request_name=request.name if request.name is not None else None,
                sheet_gid=request.sheet_gid or job.sheet_gid,
                gateway=gateway,
            )

            if incoming_schedule is not None or not job.next_run_at:
                job.next_run_at = SchedulerService()._calculate_next_run(
                    schedule_value, datetime.utcnow()
                )

            job.schedule = schedule_value
            job.job_type = job_type
            job.config = config
            job.sheet_gid = sheet_gid_value if sheet_gid_value is not None else None
            job.updated_at = datetime.utcnow()
            await db.commit()

            return {
                "ok": True,
                "data": {
                    "id": job.id,
                    "name": config.get("name"),
                    "job_type": job.job_type,
                    "schedule": job.schedule,
                    "next_run_at": job.next_run_at.isoformat()
                    if job.next_run_at
                    else None,
                },
            }

        except HTTPException:
            raise
        except Exception:
            raise HTTPException(500, "Failed to update schedule")


@router.post("/{schedule_id}/pause")
async def pause_schedule(schedule_id: str, api_ctx=Depends(require_api_key)):
    """Pause a scheduled job."""
    from ...db import get_session
    from ...models import ScheduledJob
    from sqlalchemy import select

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    async for db in get_session():
        try:
            result = await db.execute(
                select(ScheduledJob).where(
                    ScheduledJob.id == schedule_id,
                    ScheduledJob.account_id == str(account_id),
                )
            )

            job = result.scalar_one_or_none()
            if not job:
                raise HTTPException(404, "Schedule not found")

            job.is_active = False
            job.updated_at = datetime.utcnow()
            await db.commit()

            return {"ok": True}

        except HTTPException:
            raise
        except Exception as e:
            raise HTTPException(500, "Failed to pause schedule")


@router.post("/{schedule_id}/resume")
async def resume_schedule(schedule_id: str, api_ctx=Depends(require_api_key)):
    """Resume a paused scheduled job."""
    from ...db import get_session
    from ...models import ScheduledJob
    from sqlalchemy import select

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    async for db in get_session():
        try:
            result = await db.execute(
                select(ScheduledJob).where(
                    ScheduledJob.id == schedule_id,
                    ScheduledJob.account_id == str(account_id),
                )
            )

            job = result.scalar_one_or_none()
            if not job:
                raise HTTPException(404, "Schedule not found")

            job.is_active = True
            job.paused_by_system = False
            job.pause_reason = None
            job.updated_at = datetime.utcnow()
            await db.commit()

            return {"ok": True}

        except HTTPException:
            raise
        except Exception as e:
            raise HTTPException(500, "Failed to resume schedule")


@router.post("/{schedule_id}/run")
async def run_schedule_now(schedule_id: str, api_ctx=Depends(require_api_key)):
    """Run a scheduled job immediately."""
    from ...services.scheduler_service import get_scheduler
    from ...db import get_session
    from ...models import ScheduledJob
    from sqlalchemy import select

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    async for db in get_session():
        try:
            result = await db.execute(
                select(ScheduledJob).where(
                    ScheduledJob.id == schedule_id,
                    ScheduledJob.account_id == str(account_id),
                )
            )

            job = result.scalar_one_or_none()
            if not job:
                raise HTTPException(404, "Schedule not found")

            # Execute job immediately
            scheduler = await get_scheduler()
            execution = await scheduler._execute_job(job, db)
            if not execution.get("ok", False):
                return {
                    "ok": False,
                    "message": "Job execution failed",
                    "error": execution.get("error") or "Unknown error",
                    "status": execution.get("status") or "failed",
                    "run_id": execution.get("run_id"),
                }

            return {
                "ok": True,
                "message": "Job executed successfully",
                "status": execution.get("status") or "success",
                "run_id": execution.get("run_id"),
            }

        except HTTPException:
            raise
        except Exception as e:
            raise HTTPException(500, "Failed to run schedule")


@router.post("/{schedule_id}/dry-run")
async def dry_run_schedule(
    schedule_id: str, limit: int = 50, api_ctx=Depends(require_api_key)
):
    """Execute a schedule once without writing to Sheets."""
    from ...db import get_session
    from ...models import ScheduledJob
    from ...services.salesforce_gateway import SalesforceGateway
    from ...middleware.quota_checker import get_quota
    from sqlalchemy import select

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    limit = max(1, min(limit, 200))

    async for db in get_session():
        result = await db.execute(
            select(ScheduledJob).where(
                ScheduledJob.id == schedule_id,
                ScheduledJob.account_id == str(account_id),
            )
        )

        job = result.scalar_one_or_none()
        if not job:
            raise HTTPException(404, "Schedule not found")

        config = (
            json.loads(job.config)
            if isinstance(job.config, str)
            else (job.config or {})
        )

        if job.job_type != "soql_query":
            raise HTTPException(501, "Dry run currently supports SOQL schedules only")

        soql = config.get("soql")
        if not soql:
            raise HTTPException(400, "Schedule is missing a SOQL query")

        gateway = await SalesforceGateway.for_tenant(job.account_id, db)
        quota = await get_quota(job.account_id, db)
        max_rows = quota.get_limits()["rows_per_run"]
        preview_limit = min(limit, max_rows)

        records = []
        truncated = False
        async for batch in gateway.paginate_soql(soql):
            records.extend(batch)
            if len(records) >= preview_limit:
                records = records[:preview_limit]
                truncated = True
                break

        headers, rows = _records_to_sheet_table(records, soql=soql)

        return {
            "ok": True,
            "data": {
                "headers": headers,
                "rows": rows,
                "row_count": len(rows),
                "limit": preview_limit,
                "more_available": truncated,
                "soql": soql,
            },
        }


@router.post("/pause-all")
async def pause_all_schedules(api_ctx=Depends(require_api_key)):
    """Pause all active schedules for current user."""
    from ...db import get_session
    from ...models import ScheduledJob
    from sqlalchemy import update

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    async for db in get_session():
        stmt = (
            update(ScheduledJob)
            .where(
                ScheduledJob.account_id == str(account_id),
                ScheduledJob.is_active == True,
                ScheduledJob.paused_by_system == False,
            )
            .values(
                is_active=False,
                pause_reason="Paused by user",
                updated_at=datetime.utcnow(),
            )
        )
        result = await db.execute(stmt)
        await db.commit()

        return {"ok": True, "paused": result.rowcount if result else 0}


@router.post("/resume-all")
async def resume_all_schedules(api_ctx=Depends(require_api_key)):
    """Resume all user-paused schedules."""
    from ...db import get_session
    from ...models import ScheduledJob
    from sqlalchemy import update

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    async for db in get_session():
        stmt = (
            update(ScheduledJob)
            .where(
                ScheduledJob.account_id == str(account_id),
                ScheduledJob.is_active == False,
                ScheduledJob.paused_by_system == False,
            )
            .values(is_active=True, pause_reason=None, updated_at=datetime.utcnow())
        )
        result = await db.execute(stmt)
        await db.commit()

        return {"ok": True, "resumed": result.rowcount if result else 0}


@router.delete("/{schedule_id}")
async def delete_schedule(schedule_id: str, api_ctx=Depends(require_api_key)):
    """Delete a scheduled job."""
    from ...db import get_session
    from ...models import ScheduledJob, JobRun, JobRunDLQ
    from sqlalchemy import select, delete

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    async for db in get_session():
        try:
            result = await db.execute(
                select(ScheduledJob).where(
                    ScheduledJob.id == schedule_id,
                    ScheduledJob.account_id == str(account_id),
                )
            )

            job = result.scalar_one_or_none()
            if not job:
                raise HTTPException(404, "Schedule not found")

            # Remove dependent execution records first to avoid FK constraint failures.
            deleted_runs_result = await db.execute(
                delete(JobRun).where(JobRun.job_id == schedule_id)
            )
            deleted_dlq_result = await db.execute(
                delete(JobRunDLQ).where(JobRunDLQ.job_id == schedule_id)
            )

            await db.delete(job)
            await db.commit()

            return {
                "ok": True,
                "deleted_runs": deleted_runs_result.rowcount or 0,
                "deleted_dlq": deleted_dlq_result.rowcount or 0,
            }

        except HTTPException:
            raise
        except Exception as e:
            raise HTTPException(500, "Failed to delete schedule")


@router.get("/{schedule_id}/history")
async def get_schedule_history(
    schedule_id: str, limit: int = 50, api_ctx=Depends(require_api_key)
):
    """Get execution history for a scheduled job."""
    from ...db import get_session
    from ...models import ScheduledJob, JobRun
    from sqlalchemy import select

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    async for db in get_session():
        try:
            # Verify job belongs to account
            result = await db.execute(
                select(ScheduledJob).where(
                    ScheduledJob.id == schedule_id,
                    ScheduledJob.account_id == str(account_id),
                )
            )

            job = result.scalar_one_or_none()
            if not job:
                raise HTTPException(404, "Schedule not found")

            # Get run history
            runs_result = await db.execute(
                select(JobRun)
                .where(JobRun.job_id == schedule_id)
                .order_by(JobRun.started_at.desc())
                .limit(limit)
            )

            runs = runs_result.scalars().all()

            return {
                "ok": True,
                "data": {
                    "schedule_id": schedule_id,
                    "schedule_name": job.config.get("name")
                    if isinstance(job.config, dict)
                    else job.job_type,
                    "runs": [
                        {
                            "id": run.id,
                            "status": run.status,
                            "started_at": run.started_at.isoformat()
                            if run.started_at
                            else None,
                            "completed_at": run.completed_at.isoformat()
                            if run.completed_at
                            else None,
                            "duration_ms": run.duration_ms,
                            "rows_returned": run.rows_returned,
                            "error_message": run.error_message,
                            "output_location": run.output_location,
                        }
                        for run in runs
                    ],
                },
            }

        except HTTPException:
            raise
        except Exception as e:
            raise HTTPException(500, "Failed to get history")
