from __future__ import annotations

import json
import tempfile
from typing import Any, Dict, Optional

from fastapi import APIRouter, Depends, File, Form, Query, Request, UploadFile

from grantflow.api.errors import raise_agent_error
from grantflow.api.idempotency_store_facade import (
    _ingest_inventory,
    _list_ingest_events,
    _record_ingest_event,
)
from grantflow.api.demo_presets import list_ingest_preset_summaries, load_ingest_preset
from grantflow.api.preflight_service import _build_generate_preflight
from grantflow.api.presets_service import _resolve_preflight_request_context
from grantflow.api.public_views import public_ingest_inventory_payload, public_ingest_recent_payload
from grantflow.api.schemas import (
    GeneratePreflightPublicResponse,
    IngestInventoryPublicResponse,
    IngestPresetDetailPublicResponse,
    IngestPresetListPublicResponse,
    IngestReadinessRequest,
    IngestRecentListPublicResponse,
    IngestTextRequest,
)
from grantflow.api.rate_limit import rate_limit
from grantflow.api.security import require_api_key_if_configured
from grantflow.api.tenant import _check_tenant_quota, _resolve_tenant_id, _tenant_rag_namespace
from grantflow.core.strategies.factory import DonorFactory
from grantflow.memory_bank.ingest import ingest_pdf_to_namespace, ingest_text_to_namespace
from grantflow.memory_bank.vector_store import vector_store

ingest_router = APIRouter(tags=["ingest"])


@ingest_router.get(
    "/ingest/presets",
    response_model=IngestPresetListPublicResponse,
    response_model_exclude_none=True,
)
def list_ingest_presets():
    return {"presets": list_ingest_preset_summaries()}


@ingest_router.get(
    "/ingest/presets/{preset_key}",
    response_model=IngestPresetDetailPublicResponse,
    response_model_exclude_none=True,
)
def get_ingest_preset(preset_key: str):
    try:
        return load_ingest_preset(preset_key)
    except ValueError as exc:
        raise_agent_error(
            status_code=404,
            code="ingest_preset_not_found",
            message=str(exc),
            retryable=False,
            next_action="Check available ingest presets at GET /ingest/presets.",
            preset_key=preset_key,
        )


@ingest_router.post(
    "/ingest/readiness",
    response_model=GeneratePreflightPublicResponse,
    response_model_exclude_none=True,
)
def ingest_readiness(req: IngestReadinessRequest, request: Request):
    require_api_key_if_configured(request, for_read=True)
    donor, strategy, client_metadata = _resolve_preflight_request_context(
        request=request,
        donor_id=req.donor_id,
        tenant_id=req.tenant_id,
        client_metadata=req.client_metadata,
        input_context=req.input_context,
        expected_doc_families=req.expected_doc_families,
    )
    return _build_generate_preflight(
        donor_id=donor,
        strategy=strategy,
        client_metadata=client_metadata,
        architect_rag_enabled=bool(req.architect_rag_enabled),
    )


@ingest_router.get("/ingest/recent", response_model=IngestRecentListPublicResponse, response_model_exclude_none=True)
def list_recent_ingests(
    request: Request,
    donor_id: Optional[str] = Query(default=None),
    tenant_id: Optional[str] = Query(default=None),
    limit: int = Query(default=50, ge=1, le=200),
):
    require_api_key_if_configured(request, for_read=True)
    resolved_tenant_id = _resolve_tenant_id(request, explicit_tenant=tenant_id, require_if_enabled=True)
    rows = _list_ingest_events(donor_id=donor_id, tenant_id=resolved_tenant_id, limit=limit)
    return public_ingest_recent_payload(rows, donor_id=(donor_id or None), tenant_id=resolved_tenant_id)


@ingest_router.get("/ingest/inventory", response_model=IngestInventoryPublicResponse, response_model_exclude_none=True)
def get_ingest_inventory(
    request: Request,
    donor_id: Optional[str] = Query(default=None),
    tenant_id: Optional[str] = Query(default=None),
):
    require_api_key_if_configured(request, for_read=True)
    resolved_tenant_id = _resolve_tenant_id(request, explicit_tenant=tenant_id, require_if_enabled=True)
    rows = _ingest_inventory(donor_id=donor_id, tenant_id=resolved_tenant_id)
    return public_ingest_inventory_payload(rows, donor_id=(donor_id or None), tenant_id=resolved_tenant_id)


@ingest_router.post(
    "/ingest/text",
    dependencies=[Depends(rate_limit(limit=30, window_seconds=60, endpoint="ingest_text"))],
)
def ingest_text(req: IngestTextRequest, request: Request):
    """Ingest pre-extracted text into the RAG corpus.

    Agent-native alternative to PDF upload — no multipart encoding required.
    Supply raw text; the server chunks, embeds, and stores it.
    """
    require_api_key_if_configured(request, required_scope="ingest:write")

    donor = (req.donor_id or "").strip()
    if not donor:
        raise_agent_error(
            status_code=400,
            code="missing_donor_id",
            message="donor_id is required.",
            retryable=False,
            next_action="Set donor_id to a supported donor such as 'usaid' or 'eu_intpa'.",
        )

    try:
        strategy = DonorFactory.get_strategy(donor)
    except ValueError as exc:
        raise_agent_error(
            status_code=400,
            code="unsupported_donor",
            message=str(exc),
            retryable=False,
            next_action="Use a donor_id from the supported donor list.",
            donor_id=donor,
        )

    resolved_tenant_id = _resolve_tenant_id(
        request,
        explicit_tenant=req.tenant_id,
        client_metadata=req.metadata,
        require_if_enabled=True,
    )
    _check_tenant_quota(resolved_tenant_id, quota_type="ingest")
    namespace = _tenant_rag_namespace(strategy.get_rag_collection(), resolved_tenant_id)
    namespace_normalized = vector_store.normalize_namespace(namespace)

    extra_metadata: Dict[str, Any] = {
        "donor_id": donor,
        "namespace_normalized": namespace_normalized,
        "ingest_method": "text",
    }
    if resolved_tenant_id:
        extra_metadata["tenant_id"] = resolved_tenant_id
    if req.doc_family:
        extra_metadata["doc_family"] = req.doc_family
    if req.metadata:
        extra_metadata.update(req.metadata)

    try:
        result = ingest_text_to_namespace(
            req.text,
            namespace=namespace,
            title=req.title,
            chunk_size=req.chunk_size,
            overlap=req.overlap,
            metadata=extra_metadata,
        )
    except Exception as exc:
        raise_agent_error(
            status_code=500,
            code="ingest_failed",
            message=f"Text ingest failed: {exc}",
            retryable=True,
            next_action="Check your text content and retry. Contact support if the problem persists.",
            donor_id=donor,
        )

    _record_ingest_event(
        donor_id=donor,
        namespace=namespace,
        filename=req.title,
        content_type="text/plain",
        metadata=extra_metadata,
        result=result,
    )

    return {
        "status": "ingested",
        "donor_id": donor,
        "tenant_id": resolved_tenant_id,
        "namespace": namespace,
        "namespace_normalized": namespace_normalized,
        "title": req.title,
        "result": result,
    }


@ingest_router.post(
    "/ingest",
    dependencies=[Depends(rate_limit(limit=30, window_seconds=60, endpoint="ingest"))],
)
async def ingest_pdf(
    request: Request,
    donor_id: str = Form(...),
    tenant_id: Optional[str] = Form(None),
    file: UploadFile = File(...),
    metadata_json: Optional[str] = Form(None),
):
    require_api_key_if_configured(request, required_scope="ingest:write")

    donor = (donor_id or "").strip()
    if not donor:
        raise_agent_error(
            status_code=400,
            code="missing_donor_id",
            message="donor_id is required.",
            retryable=False,
            next_action="Set donor_id to a supported donor such as 'usaid' or 'eu_intpa'.",
        )

    try:
        strategy = DonorFactory.get_strategy(donor)
    except ValueError as exc:
        raise_agent_error(
            status_code=400,
            code="unsupported_donor",
            message=str(exc),
            retryable=False,
            next_action="Use a donor_id from the supported donor list.",
            donor_id=donor,
        )

    filename = (file.filename or "").strip()
    if not filename:
        raise_agent_error(
            status_code=400,
            code="missing_filename",
            message="Uploaded file must have a name.",
            retryable=False,
            next_action="Provide a valid filename for the uploaded file.",
        )
    if not filename.lower().endswith(".pdf"):
        raise_agent_error(
            status_code=400,
            code="unsupported_file_type",
            message="Only PDF uploads are supported.",
            retryable=False,
            next_action="Upload a .pdf file.",
            filename=filename,
        )

    content_type = (file.content_type or "").lower().strip()
    allowed_content_types = {"", "application/pdf", "application/x-pdf", "application/octet-stream"}
    if content_type not in allowed_content_types:
        raise_agent_error(
            status_code=400,
            code="unsupported_content_type",
            message=f"Unsupported content type: {content_type}.",
            retryable=False,
            next_action="Upload the file as application/pdf.",
            content_type=content_type,
        )

    metadata: Optional[Dict[str, Any]] = None
    if metadata_json:
        try:
            parsed = json.loads(metadata_json)
        except json.JSONDecodeError as exc:
            raise_agent_error(
                status_code=400,
                code="invalid_metadata_json",
                message=f"metadata_json is not valid JSON: {exc.msg}",
                retryable=False,
                next_action="Fix the JSON syntax in metadata_json.",
            )
        if not isinstance(parsed, dict):
            raise_agent_error(
                status_code=400,
                code="invalid_metadata_shape",
                message="metadata_json must decode to a JSON object (dict).",
                retryable=False,
                next_action='Wrap the metadata in a JSON object: {"key": "value"}.',
            )
        metadata = parsed

    content = await file.read()
    if not content:
        raise_agent_error(
            status_code=400,
            code="empty_file",
            message="Uploaded file is empty.",
            retryable=False,
            next_action="Upload a non-empty PDF file.",
            filename=filename,
        )

    resolved_tenant_id = _resolve_tenant_id(
        request,
        explicit_tenant=tenant_id,
        client_metadata=metadata,
        require_if_enabled=True,
    )
    _check_tenant_quota(resolved_tenant_id, quota_type="ingest")
    namespace = _tenant_rag_namespace(strategy.get_rag_collection(), resolved_tenant_id)
    namespace_normalized = vector_store.normalize_namespace(namespace)
    upload_metadata: Dict[str, Any] = {
        "uploaded_filename": filename,
        "uploaded_content_type": content_type or "application/pdf",
        "donor_id": donor,
        "namespace_normalized": namespace_normalized,
    }
    if resolved_tenant_id:
        upload_metadata["tenant_id"] = resolved_tenant_id
    if metadata:
        upload_metadata.update(metadata)

    tmp_path: Optional[str] = None
    try:
        with tempfile.NamedTemporaryFile(prefix="grantflow_ingest_", suffix=".pdf", delete=False) as tmp:
            tmp.write(content)
            tmp_path = tmp.name
        result = ingest_pdf_to_namespace(tmp_path, namespace=namespace, metadata=upload_metadata)
    except Exception as exc:
        raise_agent_error(
            status_code=500,
            code="ingest_failed",
            message=f"Ingest failed: {exc}",
            retryable=True,
            next_action="Check the uploaded file and retry. Contact support if the problem persists.",
            filename=filename,
            donor_id=donor,
        )
    finally:
        if tmp_path:
            try:
                import os

                os.unlink(tmp_path)
            except FileNotFoundError:
                pass

    result_payload = result if isinstance(result, dict) else {"raw_result": str(result)}
    _record_ingest_event(
        donor_id=donor,
        namespace=namespace,
        filename=filename,
        content_type=content_type or "application/pdf",
        metadata=upload_metadata,
        result=result_payload,
    )

    return {
        "status": "ingested",
        "donor_id": donor,
        "tenant_id": resolved_tenant_id,
        "namespace": namespace,
        "namespace_normalized": namespace_normalized,
        "filename": filename,
        "result": result_payload,
    }
