"""API + panel routes for remote IMAP mailbox import."""

from __future__ import annotations

import json
import logging
from typing import List, Optional

from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, UploadFile
from fastapi.responses import JSONResponse, PlainTextResponse
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator, model_validator
from sqlalchemy.orm import Session, joinedload

from .. import models
from ..admin_ops import panel_template_dir
from ..cache import get_cache
from ..db import get_db
from ..mailbox_import import (
    AUTH_MODES,
    BULK_IMPORT_MAX_ENTRIES,
    BULK_IMPORT_MAX_FILE_BYTES,
    PROTOCOLS,
    SourceConfig,
    bulk_import_template_csv,
    bulk_import_template_json,
    create_bulk_jobs,
    create_job,
    default_port_for,
    delete_job,
    ensure_schema,
    ensure_worker_started,
    get_job,
    list_jobs,
    normalize_auth_mode,
    normalize_protocol,
    parse_bulk_import,
    request_cancel,
    serialize_job,
    test_connection,
)
from ..security import pop_panel_flash, require_admin, require_panel_csrf, require_panel_session, set_panel_flash
from ..settings import get_settings
from ..utils import normalize_domain, validate_local_part

logger = logging.getLogger(__name__)
router = APIRouter(tags=["mailbox-import"])
settings = get_settings()
templates = Jinja2Templates(directory=str(panel_template_dir()))


class MailboxImportCreate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    destination_account_id: int = Field(..., ge=1)
    source_host: str = Field(..., min_length=1, max_length=255)
    source_port: Optional[int] = Field(default=None, ge=1, le=65535)
    source_protocol: str = Field(default="imaps")
    source_username: str = Field(..., min_length=1, max_length=320)
    source_password: str = Field(..., min_length=1, max_length=512)
    source_auth_mode: str = Field(default="login")

    @field_validator("source_protocol")
    @classmethod
    def validate_protocol(cls, value: str) -> str:
        return normalize_protocol(value)

    @field_validator("source_auth_mode")
    @classmethod
    def validate_auth(cls, value: str) -> str:
        return normalize_auth_mode(value)

    @field_validator("source_host")
    @classmethod
    def validate_host(cls, value: str) -> str:
        host = value.strip()
        if not host or any(c in host for c in " \t\r\n"):
            raise ValueError("Invalid source host")
        return host


class MailboxImportTest(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    source_host: str = Field(..., min_length=1, max_length=255)
    source_port: Optional[int] = Field(default=None, ge=1, le=65535)
    source_protocol: str = Field(default="imaps")
    source_username: str = Field(..., min_length=1, max_length=320)
    source_password: str = Field(..., min_length=1, max_length=512)
    source_auth_mode: str = Field(default="login")

    @field_validator("source_protocol")
    @classmethod
    def validate_protocol(cls, value: str) -> str:
        return normalize_protocol(value)

    @field_validator("source_auth_mode")
    @classmethod
    def validate_auth(cls, value: str) -> str:
        return normalize_auth_mode(value)


class MailboxImportBulkEntry(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    destination_email: Optional[str] = Field(default=None, max_length=320)
    destination_password: Optional[str] = Field(default=None, max_length=256)
    quota_mb: int = Field(default=2048, ge=1, le=1048576)
    source_host: str = Field(..., min_length=1, max_length=255)
    source_port: Optional[int] = Field(default=None, ge=1, le=65535)
    source_protocol: str = Field(default="imaps")
    source_username: Optional[str] = Field(default=None, max_length=320)
    source_password: str = Field(..., min_length=1, max_length=512)
    source_auth_mode: str = Field(default="login")

    @field_validator("destination_email", "destination_password", "source_username", "source_port", mode="before")
    @classmethod
    def empty_string_to_none(cls, value):
        if isinstance(value, str) and not value.strip():
            return None
        return value

    @model_validator(mode="after")
    def validate_emails_and_usernames(self) -> "MailboxImportBulkEntry":
        if not self.destination_email:
            if self.source_username and "@" in self.source_username:
                self.destination_email = self.source_username
            else:
                raise ValueError("destination_email is required when source_username is not a full email address")

        if not self.source_username:
            self.source_username = self.destination_email

        dest = self.destination_email.strip().lower()
        local, _, domain = dest.partition("@")
        if not local or not domain:
            raise ValueError("Invalid destination email address")
        try:
            validate_local_part(local)
            normalize_domain(domain)
        except ValueError as exc:
            raise ValueError(f"Invalid destination email address: {exc}") from exc
        self.destination_email = f"{local}@{normalize_domain(domain)}"
        return self

    @field_validator("source_protocol")
    @classmethod
    def validate_protocol(cls, value: str) -> str:
        return normalize_protocol(value)

    @field_validator("source_auth_mode")
    @classmethod
    def validate_auth(cls, value: str) -> str:
        return normalize_auth_mode(value)

    @field_validator("source_host")
    @classmethod
    def validate_host(cls, value: str) -> str:
        host = value.strip()
        if not host or any(c in host for c in " \t\r\n"):
            raise ValueError("Invalid source host")
        return host


class MailboxImportBulkRequest(BaseModel):
    entries: List[MailboxImportBulkEntry] = Field(..., min_length=1, max_length=BULK_IMPORT_MAX_ENTRIES)
    create_missing_accounts: bool = True


def _validation_error_detail(exc: ValidationError) -> str:
    parts = []
    for item in exc.errors()[:4]:
        loc = ".".join(str(part) for part in item.get("loc", ()) if part != "__root__")
        msg = str(item.get("msg", "invalid value"))
        parts.append(f"{loc}: {msg}" if loc else msg)
    return "; ".join(parts) or "Invalid entry"


def _process_bulk_entries(
    db: Session,
    raw_entries: list[tuple[int, dict]],
    *,
    create_missing: bool,
    cache=None,
) -> dict:
    validated: list[dict] = []
    errors: list[dict] = []
    for row, raw in raw_entries:
        try:
            entry = MailboxImportBulkEntry.model_validate(raw)
        except ValidationError as exc:
            errors.append(
                {
                    "row": row,
                    "destination_email": str(raw.get("destination_email") or ""),
                    "detail": _validation_error_detail(exc),
                }
            )
            continue
        data = entry.model_dump()
        data["row"] = row
        validated.append(data)
    result = create_bulk_jobs(db, validated, create_missing_accounts=create_missing, cache=cache)
    result["errors"] = errors + result["errors"]
    result["error_count"] = len(result["errors"])
    result["total"] = len(raw_entries)
    return result


def _read_bulk_upload(upload: UploadFile) -> bytes:
    upload.file.seek(0, 2)
    size = upload.file.tell()
    upload.file.seek(0)
    if size > BULK_IMPORT_MAX_FILE_BYTES:
        raise HTTPException(
            status_code=400,
            detail=f"Bulk import file exceeds the {BULK_IMPORT_MAX_FILE_BYTES // (1024 * 1024)} MiB limit",
        )
    content = upload.file.read()
    if not content:
        raise HTTPException(status_code=400, detail="Bulk import file is empty")
    return content


def _bulk_template_response(format: str):
    fmt = (format or "json").strip().lower()
    if fmt == "json":
        return PlainTextResponse(
            bulk_import_template_json(),
            media_type="application/json",
            headers={"Content-Disposition": 'attachment; filename="mailbox-import-template.json"'},
        )
    if fmt == "csv":
        return PlainTextResponse(
            bulk_import_template_csv(),
            media_type="text/csv",
            headers={"Content-Disposition": 'attachment; filename="mailbox-import-template.csv"'},
        )
    raise HTTPException(status_code=400, detail="Unsupported template format: use json or csv")


def _get_account(db: Session, account_id: int) -> models.Account:
    account = (
        db.query(models.Account)
        .options(joinedload(models.Account.domain))
        .filter(models.Account.id == account_id)
        .first()
    )
    if not account:
        raise HTTPException(status_code=404, detail="Destination account not found")
    if not account.is_active:
        raise HTTPException(status_code=400, detail="Destination account is suspended")
    if not account.domain or not account.domain.is_active:
        raise HTTPException(status_code=400, detail="Destination domain is inactive")
    return account


# ---------------------------------------------------------------------------
# REST API
# ---------------------------------------------------------------------------


@router.post("/mailbox-import/test", response_model=dict)
def api_test_connection(payload: MailboxImportTest, _: str = Depends(require_admin)):
    port = payload.source_port or default_port_for(payload.source_protocol)
    cfg = SourceConfig(
        host=payload.source_host,
        port=port,
        protocol=payload.source_protocol,
        username=payload.source_username,
        password=payload.source_password,
        auth_mode=payload.source_auth_mode,
    )
    try:
        return test_connection(cfg)
    except Exception as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc


@router.post("/mailbox-import", response_model=dict, status_code=201)
def api_create_import(payload: MailboxImportCreate, db: Session = Depends(get_db), _: str = Depends(require_admin)):
    ensure_schema(db)
    account = _get_account(db, payload.destination_account_id)
    try:
        job = create_job(
            db,
            account=account,
            host=payload.source_host,
            port=payload.source_port,
            protocol=payload.source_protocol,
            username=payload.source_username,
            password=payload.source_password,
            auth_mode=payload.source_auth_mode,
        )
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return serialize_job(job)


@router.get("/mailbox-import/bulk-template")
def api_bulk_import_template(format: str = Query(default="json"), _: str = Depends(require_admin)):
    """Download an example JSON/CSV file to fill in for a bulk import."""
    return _bulk_template_response(format)


@router.post("/mailbox-import/bulk", response_model=dict, status_code=201)
def api_create_bulk_import(
    payload: MailboxImportBulkRequest,
    db: Session = Depends(get_db),
    cache=Depends(get_cache),
    _: str = Depends(require_admin),
):
    """Queue one import job per entry; each job runs as a separate process."""
    ensure_schema(db)
    entries = []
    for index, entry in enumerate(payload.entries, start=1):
        data = entry.model_dump()
        data["row"] = index
        entries.append(data)
    result = create_bulk_jobs(db, entries, create_missing_accounts=payload.create_missing_accounts, cache=cache)
    return {"status": "ok", **result}


@router.post("/mailbox-import/bulk-upload", response_model=dict, status_code=201)
def api_upload_bulk_import(
    file: UploadFile = File(...),
    create_missing_accounts: bool = Form(default=False),
    db: Session = Depends(get_db),
    cache=Depends(get_cache),
    _: str = Depends(require_admin),
):
    """Upload a JSON/CSV bulk-import file and queue one job per account."""
    ensure_schema(db)
    content = _read_bulk_upload(file)
    try:
        raw_entries = parse_bulk_import(content, file.filename or "")
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    result = _process_bulk_entries(db, raw_entries, create_missing=create_missing_accounts, cache=cache)
    return {"status": "ok", **result}


@router.get("/mailbox-import", response_model=List[dict])
def api_list_imports(
    limit: int = Query(default=50, ge=1, le=500),
    status: Optional[str] = Query(default=None),
    db: Session = Depends(get_db),
    _: str = Depends(require_admin),
):
    ensure_schema(db)
    ensure_worker_started()
    jobs = list_jobs(db, limit=limit, status=status)
    return [serialize_job(job) for job in jobs]


@router.get("/mailbox-import/{job_id}", response_model=dict)
def api_get_import(job_id: int, db: Session = Depends(get_db), _: str = Depends(require_admin)):
    ensure_schema(db)
    job = get_job(db, job_id)
    if not job:
        raise HTTPException(status_code=404, detail="Import job not found")
    return serialize_job(job)


@router.post("/mailbox-import/{job_id}/cancel", response_model=dict)
def api_cancel_import(job_id: int, db: Session = Depends(get_db), _: str = Depends(require_admin)):
    job = get_job(db, job_id)
    if not job:
        raise HTTPException(status_code=404, detail="Import job not found")
    job = request_cancel(db, job)
    return serialize_job(job)


@router.delete("/mailbox-import/{job_id}", response_model=dict)
def api_delete_import(job_id: int, db: Session = Depends(get_db), _: str = Depends(require_admin)):
    job = get_job(db, job_id)
    if not job:
        raise HTTPException(status_code=404, detail="Import job not found")
    try:
        delete_job(db, job)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return {"status": "deleted", "id": job_id}


# ---------------------------------------------------------------------------
# Panel
# ---------------------------------------------------------------------------


def _panel_accounts(db: Session) -> list[dict]:
    rows = (
        db.query(models.Account)
        .options(joinedload(models.Account.domain))
        .order_by(models.Account.username.asc())
        .all()
    )
    return [
        {
            "id": row.id,
            "username": row.username,
            "is_active": bool(row.is_active),
            "domain": row.domain.name if row.domain else "",
        }
        for row in rows
        if row.domain
    ]


def _import_panel_context(request: Request, session: dict, db: Session, **extra):
    from .admin import panel_context

    ensure_schema(db)
    ensure_worker_started()
    jobs = [serialize_job(job) for job in list_jobs(db, limit=100)]
    accounts = _panel_accounts(db)
    active_tab = extra.pop("import_tab", None) or request.query_params.get("tab") or "new"
    if active_tab not in {"new", "jobs", "bulk"}:
        active_tab = "new"
    ctx = panel_context(
        request,
        session,
        "import",
        import_jobs=jobs,
        import_accounts=accounts,
        active_import_tab=active_tab,
        import_protocols=sorted(PROTOCOLS),
        import_auth_modes=sorted(AUTH_MODES),
        default_import_port=993,
        **extra,
    )
    return ctx


@router.get("/panel/import")
def panel_import(
    request: Request,
    notice: Optional[str] = None,
    error: Optional[str] = None,
    tab: Optional[str] = None,
    session: dict = Depends(require_panel_session),
    db: Session = Depends(get_db),
):
    from .admin import ensure_web_panel_enabled

    ensure_web_panel_enabled()
    notices = {
        "import-queued": "Import job queued. Progress is updated on the Jobs tab.",
        "import-cancelled": "Cancel requested for the import job.",
        "import-deleted": "Import job removed.",
        "connection-ok": "Source IMAP connection successful.",
        "bulk-import-done": "Bulk import processed. Jobs run one per account with progress on the Jobs tab.",
    }
    bulk_result = None
    bulk_result_raw = pop_panel_flash(request, session, "bulk_import_result")
    if bulk_result_raw:
        try:
            bulk_result = json.loads(bulk_result_raw)
        except json.JSONDecodeError:
            bulk_result = None
    context = _import_panel_context(
        request,
        session,
        db,
        notice=notices.get(notice, notice),
        error=error,
        import_tab=tab,
        bulk_import_result=bulk_result,
    )
    return templates.TemplateResponse(request, "panel.html", context)


@router.get("/panel/import/bulk-template")
def panel_bulk_import_template(
    format: str = Query(default="json"),
    session: dict = Depends(require_panel_session),
):
    from .admin import ensure_web_panel_enabled

    ensure_web_panel_enabled()
    return _bulk_template_response(format)


@router.post("/panel/import/bulk")
def panel_import_bulk(
    request: Request,
    csrf_token: str = Form(...),
    file: UploadFile = File(...),
    create_missing_accounts: Optional[str] = Form(None),
    session: dict = Depends(require_panel_session),
    db: Session = Depends(get_db),
    cache=Depends(get_cache),
):
    from .admin import ensure_web_panel_enabled, panel_redirect

    ensure_web_panel_enabled()
    require_panel_csrf(session, csrf_token)
    try:
        content = _read_bulk_upload(file)
    except HTTPException as exc:
        return panel_redirect("/panel/import", error=str(exc.detail), tab="bulk")
    try:
        raw_entries = parse_bulk_import(content, file.filename or "")
    except ValueError as exc:
        return panel_redirect("/panel/import", error=str(exc), tab="bulk")
    result = _process_bulk_entries(db, raw_entries, create_missing=True, cache=cache)
    set_panel_flash(request, session, "bulk_import_result", json.dumps(result, default=str)[:20000])
    if result["error_count"]:
        notice = (
            f"Bulk import processed: {result['queued_count']} job(s) queued, "
            f"{result['error_count']} row(s) skipped — see details below."
        )
    else:
        notice = "bulk-import-done"
    return panel_redirect("/panel/import", notice=notice, tab="bulk")


@router.get("/panel/import/jobs.json")
def panel_import_jobs_json(
    session: dict = Depends(require_panel_session),
    db: Session = Depends(get_db),
):
    from .admin import ensure_web_panel_enabled

    ensure_web_panel_enabled()
    ensure_schema(db)
    ensure_worker_started()
    jobs = [serialize_job(job) for job in list_jobs(db, limit=100)]
    return JSONResponse({"jobs": jobs})


@router.post("/panel/import")
def panel_import_create(
    request: Request,
    csrf_token: str = Form(...),
    destination_account_id: int = Form(...),
    source_host: str = Form(...),
    source_port: str = Form(""),
    source_protocol: str = Form("imaps"),
    source_username: str = Form(...),
    source_password: str = Form(...),
    source_auth_mode: str = Form("login"),
    action: str = Form("import"),
    session: dict = Depends(require_panel_session),
    db: Session = Depends(get_db),
):
    from .admin import ensure_web_panel_enabled, panel_redirect

    ensure_web_panel_enabled()
    require_panel_csrf(session, csrf_token)

    def fail(message: str, tab: str = "new"):
        return panel_redirect("/panel/import", error=message, tab=tab)

    try:
        protocol = normalize_protocol(source_protocol)
        auth_mode = normalize_auth_mode(source_auth_mode)
    except ValueError as exc:
        return fail(str(exc))

    port_raw = (source_port or "").strip()
    port = int(port_raw) if port_raw else default_port_for(protocol)
    if port < 1 or port > 65535:
        return fail("Port must be between 1 and 65535")

    if action == "test":
        cfg = SourceConfig(
            host=source_host.strip(),
            port=port,
            protocol=protocol,
            username=source_username.strip(),
            password=source_password,
            auth_mode=auth_mode,
        )
        try:
            result = test_connection(cfg)
        except Exception as exc:
            return fail(str(exc))
        folder_count = result.get("folder_count", 0)
        return panel_redirect(
            "/panel/import",
            notice=f"connection-ok — {folder_count} folders visible on source",
            tab="new",
        )

    account = (
        db.query(models.Account)
        .options(joinedload(models.Account.domain))
        .filter(models.Account.id == destination_account_id)
        .first()
    )
    if not account:
        return fail("Destination account not found")
    if not account.is_active:
        return fail("Destination account is suspended")
    try:
        create_job(
            db,
            account=account,
            host=source_host,
            port=port,
            protocol=protocol,
            username=source_username,
            password=source_password,
            auth_mode=auth_mode,
        )
    except ValueError as exc:
        return fail(str(exc))
    return panel_redirect("/panel/import", notice="import-queued", tab="jobs")


@router.post("/panel/import/{job_id}/cancel")
def panel_import_cancel(
    job_id: int,
    csrf_token: str = Form(...),
    session: dict = Depends(require_panel_session),
    db: Session = Depends(get_db),
):
    from .admin import ensure_web_panel_enabled, panel_redirect

    ensure_web_panel_enabled()
    require_panel_csrf(session, csrf_token)
    job = get_job(db, job_id)
    if not job:
        return panel_redirect("/panel/import", error="Import job not found", tab="jobs")
    request_cancel(db, job)
    return panel_redirect("/panel/import", notice="import-cancelled", tab="jobs")


@router.post("/panel/import/{job_id}/delete")
def panel_import_delete(
    job_id: int,
    csrf_token: str = Form(...),
    session: dict = Depends(require_panel_session),
    db: Session = Depends(get_db),
):
    from .admin import ensure_web_panel_enabled, panel_redirect

    ensure_web_panel_enabled()
    require_panel_csrf(session, csrf_token)
    job = get_job(db, job_id)
    if not job:
        return panel_redirect("/panel/import", error="Import job not found", tab="jobs")
    try:
        delete_job(db, job)
    except ValueError as exc:
        return panel_redirect("/panel/import", error=str(exc), tab="jobs")
    return panel_redirect("/panel/import", notice="import-deleted", tab="jobs")
