"""
cortex/api/routes/batch.py — HTTP routes for background batch ingestion (#1326).

Endpoints:
    POST   /api/v1/batch/upload      — Upload file and create batch job
    POST   /api/v1/batch/start/{id}  — Start processing a pending job
    GET    /api/v1/batch/status/{id} — Get job progress
    GET    /api/v1/batch/results/{id} — Get final results
    GET    /api/v1/batch/jobs        — List all batch jobs
    GET    /api/v1/batch/worker-status — Check background worker health
    DELETE /api/v1/batch/{id}        — Cancel a running job
"""
from __future__ import annotations

import json
import logging
import os
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional

from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, UploadFile, status

from cortex.api.deps import PREFIX, get_plugin, _resolve_owner_id

logger = logging.getLogger(__name__)

router = APIRouter()

# Shared holder for the batch worker instance (set by http_server lifespan)
batch_worker_holder: dict = {}

_ALLOWED_EXTENSIONS = {".md", ".txt", ".jsonl"}
_MAX_UPLOAD_BYTES = 50_000_000  # 50 MB


def _get_db_path() -> str:
    """Resolve the batch_jobs database path (same DB as main Cortex)."""
    data_dir = os.environ.get("CORTEX_DATA_DIR", "/tmp/cortex-data")
    return os.environ.get("CORTEX_DB_PATH", os.path.join(data_dir, "cortex.db"))


def _verify_job_ownership(job: dict, auth_owner: str, job_id: str) -> None:
    """Raise 404 if the job doesn't belong to auth_owner (unless open-access)."""
    config = json.loads(job.get("config_json") or "{}")
    job_owner = config.get("owner_id", "default")
    if auth_owner != "default" and job_owner != auth_owner:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Job {job_id} not found",
        )


def _make_routes(verify_api_key):
    """Register all batch ingestion routes."""

    @router.post(
        f"{PREFIX}/batch/upload",
        status_code=status.HTTP_201_CREATED,
        summary="Upload file and create batch ingestion job",
        tags=["batch"],
    )
    async def upload_batch(
        request: Request,
        file: UploadFile = File(...),
        owner_id: str = Query(..., description="Memory owner namespace (required). Cannot be 'default'."),
        auto_start: bool = Query(True),
        exclude_categories: Optional[str] = Query(None, description="Comma-separated categories to exclude from stored claims (e.g. 'decisions,engineering')"),
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ):
        """Upload a file (.md, .txt, .jsonl) for batch ingestion.

        Creates a batch job and optionally auto-starts processing.
        JSONL files are treated as Mem0 format (one memory per line).
        """
        import aiosqlite

        # Resolve owner from auth — ignore query param override when authenticated (#818)
        resolved_owner = _resolve_owner_id(request, owner_id)
        if owner_id and resolved_owner != owner_id:
            logger.warning(
                "Owner ID override in upload_batch: requested=%s resolved=%s ip=%s",
                owner_id, resolved_owner, request.client.host if request.client else "unknown",
            )
        owner_id = resolved_owner

        # Reject 'default' owner_id — forces callers to be explicit
        if not owner_id or not owner_id.strip() or owner_id.strip() == "default":
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="owner_id is required and cannot be 'default'. "
                       "Specify an explicit owner namespace (e.g. 'eva-origin').",
            )

        filename = file.filename or "upload.txt"
        ext = Path(filename).suffix.lower()

        if ext not in _ALLOWED_EXTENSIONS:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"Unsupported file type '{ext}'. Allowed: {sorted(_ALLOWED_EXTENSIONS)}",
            )

        # Read file content with size limit
        content = await file.read()
        if len(content) > _MAX_UPLOAD_BYTES:
            raise HTTPException(
                status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
                detail=f"File too large (max {_MAX_UPLOAD_BYTES // 1_000_000} MB)",
            )

        job_id = str(uuid.uuid4())
        now = datetime.now(timezone.utc).isoformat()

        # Save file to upload directory
        data_dir = os.environ.get("CORTEX_DATA_DIR", "/tmp/cortex-data")
        upload_dir = Path(data_dir) / "batch_uploads" / job_id
        upload_dir.mkdir(parents=True, exist_ok=True)
        file_path = upload_dir / filename
        file_path.write_bytes(content)

        # Determine source type
        source_type = "mem0_jsonl" if ext == ".jsonl" else "file"

        config = {
            "filename": filename,
            "owner_id": owner_id,
            "file_size_bytes": len(content),
        }
        if exclude_categories:
            config["exclude_categories"] = [c.strip().lower() for c in exclude_categories.split(",") if c.strip()]

        initial_status = "pending" if auto_start else "created"

        db_path = _get_db_path()
        async with aiosqlite.connect(db_path) as db:
            await db.execute(
                "INSERT INTO batch_jobs "
                "(id, status, source_type, filename, config_json, created_at, updated_at) "
                "VALUES (?, ?, ?, ?, ?, ?, ?)",
                (job_id, initial_status, source_type, filename,
                 json.dumps(config), now, now),
            )
            await db.commit()

        return {
            "job_id": job_id,
            "status": initial_status,
            "filename": filename,
            "source_type": source_type,
            "file_size_bytes": len(content),
        }

    @router.post(
        f"{PREFIX}/batch/start/{{job_id}}",
        summary="Start processing a batch job",
        tags=["batch"],
    )
    async def start_batch(
        request: Request,
        job_id: str,
        _: None = Depends(verify_api_key),
    ):
        """Start (or restart) processing a batch job.

        Only works for jobs in 'created' or 'failed' status.
        """
        import aiosqlite

        auth_owner = _resolve_owner_id(request)
        db_path = _get_db_path()
        async with aiosqlite.connect(db_path) as db:
            db.row_factory = aiosqlite.Row
            async with db.execute(
                "SELECT * FROM batch_jobs WHERE id = ?", (job_id,)
            ) as cur:
                row = await cur.fetchone()
                if not row:
                    raise HTTPException(
                        status_code=status.HTTP_404_NOT_FOUND,
                        detail=f"Job {job_id} not found",
                    )

                job = dict(row)
                _verify_job_ownership(job, auth_owner, job_id)

                if job["status"] not in ("created", "failed"):
                    raise HTTPException(
                        status_code=status.HTTP_409_CONFLICT,
                        detail=f"Job is '{job['status']}', cannot start",
                    )

            now = datetime.now(timezone.utc).isoformat()
            await db.execute(
                "UPDATE batch_jobs SET status = 'pending', updated_at = ? WHERE id = ?",
                (now, job_id),
            )
            await db.commit()

        return {"job_id": job_id, "status": "pending"}

    @router.get(
        f"{PREFIX}/batch/status/{{job_id}}",
        summary="Get batch job progress",
        tags=["batch"],
    )
    async def get_batch_status(
        request: Request,
        job_id: str,
        _: None = Depends(verify_api_key),
    ):
        """Get current progress of a batch job."""
        import aiosqlite

        auth_owner = _resolve_owner_id(request)
        db_path = _get_db_path()
        async with aiosqlite.connect(db_path) as db:
            db.row_factory = aiosqlite.Row
            async with db.execute(
                "SELECT * FROM batch_jobs WHERE id = ?", (job_id,)
            ) as cur:
                row = await cur.fetchone()
                if not row:
                    raise HTTPException(
                        status_code=status.HTTP_404_NOT_FOUND,
                        detail=f"Job {job_id} not found",
                    )

                job = dict(row)

        _verify_job_ownership(job, auth_owner, job_id)

        total = job["total_chunks"] or 0
        processed = job["processed_chunks"] or 0
        pct = round((processed / total) * 100, 1) if total > 0 else 0.0

        return {
            "job_id": job_id,
            "status": job["status"],
            "filename": job["filename"],
            "source_type": job["source_type"],
            "total_chunks": total,
            "processed_chunks": processed,
            "claims_stored": job["claims_stored"] or 0,
            "error_count": len(json.loads(job["errors_json"] or "[]")),
            "percent_complete": pct,
            "created_at": job["created_at"],
            "updated_at": job["updated_at"],
            "completed_at": job["completed_at"],
        }

    @router.get(
        f"{PREFIX}/batch/results/{{job_id}}",
        summary="Get batch job results",
        tags=["batch"],
    )
    async def get_batch_results(
        request: Request,
        job_id: str,
        _: None = Depends(verify_api_key),
    ):
        """Get final results of a completed batch job with per-file breakdown."""
        import aiosqlite

        auth_owner = _resolve_owner_id(request)
        db_path = _get_db_path()
        async with aiosqlite.connect(db_path) as db:
            db.row_factory = aiosqlite.Row
            async with db.execute(
                "SELECT * FROM batch_jobs WHERE id = ?", (job_id,)
            ) as cur:
                row = await cur.fetchone()
                if not row:
                    raise HTTPException(
                        status_code=status.HTTP_404_NOT_FOUND,
                        detail=f"Job {job_id} not found",
                    )

                job = dict(row)

        _verify_job_ownership(job, auth_owner, job_id)

        errors = json.loads(job["errors_json"] or "[]")
        file_results = json.loads(job["file_results_json"] or "[]")

        return {
            "job_id": job_id,
            "status": job["status"],
            "filename": job["filename"],
            "source_type": job["source_type"],
            "total_chunks": job["total_chunks"] or 0,
            "processed_chunks": job["processed_chunks"] or 0,
            "claims_stored": job["claims_stored"] or 0,
            "errors": errors,
            "file_results": file_results,
            "created_at": job["created_at"],
            "updated_at": job["updated_at"],
            "completed_at": job["completed_at"],
        }

    @router.get(
        f"{PREFIX}/batch/jobs",
        summary="List all batch jobs",
        tags=["batch"],
    )
    async def list_batch_jobs(
        request: Request,
        job_status: Optional[str] = Query(None, alias="status"),
        limit: int = Query(50, ge=1, le=500),
        offset: int = Query(0, ge=0),
        _: None = Depends(verify_api_key),
    ):
        """List batch jobs with optional status filter.

        Results are scoped to the authenticated user's owner_id (#818).
        """
        import aiosqlite

        auth_owner = _resolve_owner_id(request)
        db_path = _get_db_path()
        async with aiosqlite.connect(db_path) as db:
            db.row_factory = aiosqlite.Row

            if job_status:
                query = (
                    "SELECT * FROM batch_jobs WHERE status = ? "
                    "ORDER BY created_at DESC LIMIT ? OFFSET ?"
                )
                params = (job_status, limit, offset)
            else:
                query = (
                    "SELECT * FROM batch_jobs "
                    "ORDER BY created_at DESC LIMIT ? OFFSET ?"
                )
                params = (limit, offset)

            async with db.execute(query, params) as cur:
                rows = await cur.fetchall()

        jobs = []
        for row in rows:
            job = dict(row)
            # Filter by owner — owner_id is stored in config_json (#818)
            config = json.loads(job.get("config_json") or "{}")
            job_owner = config.get("owner_id", "default")
            if auth_owner != "default" and job_owner != auth_owner:
                continue
            total = job["total_chunks"] or 0
            processed = job["processed_chunks"] or 0
            pct = round((processed / total) * 100, 1) if total > 0 else 0.0
            jobs.append({
                "job_id": job["id"],
                "status": job["status"],
                "filename": job["filename"],
                "source_type": job["source_type"],
                "total_chunks": total,
                "processed_chunks": processed,
                "claims_stored": job["claims_stored"] or 0,
                "percent_complete": pct,
                "created_at": job["created_at"],
                "completed_at": job["completed_at"],
            })

        return {"jobs": jobs, "count": len(jobs)}

    @router.delete(
        f"{PREFIX}/batch/{{job_id}}",
        summary="Cancel a batch job",
        tags=["batch"],
    )
    async def cancel_batch(
        request: Request,
        job_id: str,
        _: None = Depends(verify_api_key),
    ):
        """Cancel a running or pending batch job."""
        import aiosqlite

        auth_owner = _resolve_owner_id(request)
        db_path = _get_db_path()
        async with aiosqlite.connect(db_path) as db:
            db.row_factory = aiosqlite.Row
            async with db.execute(
                "SELECT * FROM batch_jobs WHERE id = ?", (job_id,)
            ) as cur:
                row = await cur.fetchone()
                if not row:
                    raise HTTPException(
                        status_code=status.HTTP_404_NOT_FOUND,
                        detail=f"Job {job_id} not found",
                    )

                job = dict(row)
                _verify_job_ownership(job, auth_owner, job_id)

                if job["status"] in ("completed", "cancelled"):
                    raise HTTPException(
                        status_code=status.HTTP_409_CONFLICT,
                        detail=f"Job is already '{job['status']}'",
                    )

            now = datetime.now(timezone.utc).isoformat()
            await db.execute(
                "UPDATE batch_jobs SET status = 'cancelled', updated_at = ? WHERE id = ?",
                (now, job_id),
            )
            await db.commit()

        return {"job_id": job_id, "status": "cancelled"}

    @router.get(
        f"{PREFIX}/batch/worker-status",
        summary="Check batch worker health",
        tags=["batch"],
    )
    async def worker_status(
        _: None = Depends(verify_api_key),
    ):
        """Return the current status of the background batch ingest worker.

        Useful for diagnosing why jobs might be stuck at 'pending'.
        """
        worker = batch_worker_holder.get("worker")
        if worker is None:
            return {
                "alive": False,
                "state": "not_configured",
                "detail": "Batch worker was not started (no worker instance)",
            }
        return worker.status
