"""mail_summary API — local LLM weekly mail work summary."""

from __future__ import annotations

import asyncio
import json
import logging
import shutil
import uuid
from contextlib import asynccontextmanager
from datetime import date
from pathlib import Path
from typing import Literal

from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field

from config import CORS_ORIGINS, LLM_MODEL, UPLOAD_DIR
from db.pool import close_pool, init_pool, pool_enabled
from job_repository import create_job, ensure_tables, get_job, get_report, list_jobs
from llm_client import ping_llm
from pipeline import run_analysis_job

logger = logging.getLogger("mail_summary")
logging.basicConfig(level=logging.INFO)
logger.setLevel(logging.INFO)

MailboxScope = Literal["inbox", "sent", "both"]
Mailbox = Literal["inbox", "sent"]
STAGING_DIR = UPLOAD_DIR / "staging"


@asynccontextmanager
async def lifespan(app: FastAPI):
    UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
    STAGING_DIR.mkdir(parents=True, exist_ok=True)
    await init_pool()
    await ensure_tables()
    yield
    await close_pool()


app = FastAPI(title="Mail Summary API", version="1.0.0", lifespan=lifespan)

app.add_middleware(
    CORSMiddleware,
    allow_origins=CORS_ORIGINS or ["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


class JobCreateResponse(BaseModel):
    job: dict


class HealthResponse(BaseModel):
    status: str
    database: str
    model: str = Field(default="")
    llm: dict = Field(default_factory=dict)


class UploadResponse(BaseModel):
    upload_id: str
    mailbox: str
    filename: str
    size_bytes: int


def _staging_path(upload_id: str) -> Path:
    path = (STAGING_DIR / upload_id).resolve()
    if not str(path).startswith(str(STAGING_DIR.resolve())):
        raise HTTPException(status_code=400, detail="invalid_upload_id")
    return path


def _write_staging_meta(stage_dir: Path, meta: dict) -> None:
    (stage_dir / "meta.json").write_text(json.dumps(meta, ensure_ascii=False), encoding="utf-8")


def _read_staging_meta(upload_id: str) -> dict:
    stage_dir = _staging_path(upload_id)
    meta_path = stage_dir / "meta.json"
    if not meta_path.is_file():
        raise HTTPException(status_code=404, detail="upload_not_found")
    return json.loads(meta_path.read_text(encoding="utf-8"))


def _attach_staging_to_job(upload_id: str, job_dir: Path, mailbox: str) -> str:
    """Copy/link staged zip into a job dir, keeping staging for re-analysis."""
    meta = _read_staging_meta(upload_id)
    if meta.get("mailbox") != mailbox:
        raise HTTPException(status_code=400, detail="upload_mailbox_mismatch")
    stage_dir = _staging_path(upload_id)
    filename = meta["filename"]
    src = stage_dir / f"{mailbox}_{filename}"
    if not src.exists() and not src.is_symlink():
        raise HTTPException(status_code=404, detail="upload_file_missing")
    dest = job_dir / f"{mailbox}_{filename}"
    if dest.exists() or dest.is_symlink():
        dest.unlink()
    # Prefer symlink so multi-GB files are not duplicated.
    try:
        target = src.resolve() if src.is_symlink() else src
        dest.symlink_to(target)
    except OSError:
        shutil.copy2(src, dest)
    return filename


@app.get("/api/health", response_model=HealthResponse)
async def health() -> HealthResponse:
    llm = await ping_llm()
    return HealthResponse(
        status="ok",
        database="up" if pool_enabled() else "down",
        model=LLM_MODEL,
        llm=llm,
    )


@app.post("/api/uploads", response_model=UploadResponse)
async def api_upload_zip(
    mailbox: Mailbox = Form(...),
    file: UploadFile = File(...),
) -> UploadResponse:
    name = (file.filename or "").lower()
    if not name.endswith(".zip"):
        raise HTTPException(status_code=400, detail="file_must_be_zip")

    upload_id = uuid.uuid4().hex
    stage_dir = _staging_path(upload_id)
    stage_dir.mkdir(parents=True, exist_ok=True)
    safe_name = Path(file.filename or f"{mailbox}.zip").name
    dest = stage_dir / f"{mailbox}_{safe_name}"
    size = 0
    try:
        with dest.open("wb") as out:
            while True:
                chunk = await file.read(1024 * 1024)
                if not chunk:
                    break
                out.write(chunk)
                size += len(chunk)
    except Exception:
        shutil.rmtree(stage_dir, ignore_errors=True)
        logger.exception("Upload failed")
        raise HTTPException(status_code=500, detail="upload_failed") from None

    meta = {
        "upload_id": upload_id,
        "mailbox": mailbox,
        "filename": safe_name,
        "size_bytes": size,
    }
    _write_staging_meta(stage_dir, meta)
    return UploadResponse(**meta)


@app.delete("/api/uploads/{upload_id}")
async def api_delete_upload(upload_id: str) -> dict:
    stage_dir = _staging_path(upload_id)
    if stage_dir.is_dir():
        shutil.rmtree(stage_dir, ignore_errors=True)
    return {"ok": True, "upload_id": upload_id}


@app.get("/api/jobs")
async def api_list_jobs(limit: int = 20) -> dict:
    if not pool_enabled():
        raise HTTPException(status_code=503, detail="database_unavailable")
    jobs = await list_jobs(limit=max(1, min(limit, 100)))
    return {"jobs": jobs}


@app.get("/api/jobs/{job_id}")
async def api_get_job(job_id: int) -> dict:
    if not pool_enabled():
        raise HTTPException(status_code=503, detail="database_unavailable")
    job = await get_job(job_id)
    if not job:
        raise HTTPException(status_code=404, detail="job_not_found")
    return {"job": job}


@app.get("/api/jobs/{job_id}/report")
async def api_get_report(job_id: int) -> dict:
    if not pool_enabled():
        raise HTTPException(status_code=503, detail="database_unavailable")
    job = await get_job(job_id)
    if not job:
        raise HTTPException(status_code=404, detail="job_not_found")
    report = await get_report(job_id)
    return {"job": job, "report": report}


@app.post("/api/jobs", response_model=JobCreateResponse)
async def api_create_job(
    date_from: date = Form(...),
    date_to: date = Form(...),
    mailbox_scope: MailboxScope = Form("both"),
    model: str = Form(""),
    inbox_upload_id: str = Form(""),
    sent_upload_id: str = Form(""),
) -> JobCreateResponse:
    if not pool_enabled():
        raise HTTPException(status_code=503, detail="database_unavailable")
    if date_from > date_to:
        raise HTTPException(status_code=400, detail="invalid_date_range")

    inbox_upload_id = inbox_upload_id.strip()
    sent_upload_id = sent_upload_id.strip()

    if mailbox_scope in ("inbox", "both") and not inbox_upload_id:
        raise HTTPException(status_code=400, detail="inbox_zip_required")
    if mailbox_scope in ("sent", "both") and not sent_upload_id:
        raise HTTPException(status_code=400, detail="sent_zip_required")

    selected_model = (model or LLM_MODEL).strip() or LLM_MODEL
    job_token = uuid.uuid4().hex
    job_dir = UPLOAD_DIR / job_token
    job_dir.mkdir(parents=True, exist_ok=True)

    try:
        inbox_name = (
            _attach_staging_to_job(inbox_upload_id, job_dir, "inbox") if inbox_upload_id else None
        )
        sent_name = (
            _attach_staging_to_job(sent_upload_id, job_dir, "sent") if sent_upload_id else None
        )
        job = await create_job(
            mailbox_scope=mailbox_scope,
            date_from=date_from,
            date_to=date_to,
            model=selected_model,
            inbox_zip_name=inbox_name,
            sent_zip_name=sent_name,
        )
        final_dir = UPLOAD_DIR / str(job["id"])
        if final_dir.exists():
            shutil.rmtree(final_dir)
        job_dir.rename(final_dir)
    except HTTPException:
        shutil.rmtree(job_dir, ignore_errors=True)
        raise
    except Exception:
        shutil.rmtree(job_dir, ignore_errors=True)
        logger.exception("Failed to create analysis job")
        raise HTTPException(status_code=500, detail="job_create_failed") from None

    asyncio.create_task(run_analysis_job(job["id"]))
    return JobCreateResponse(job=job)
