from __future__ import annotations

import json
import time
import uuid
from typing import Any, Dict, List, Literal, Optional

from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator

from .deps import require_api_key
from ...db import get_session
from ...repos.salesforce_ai_reports_repo import SalesforceAIReportsRepo
from ...services.ai_rate_limiter import OrgLimiter
from ...services.cache import HybridCache
from ...services.salesforce_ai_reports import (
    PlannerUnavailable,
    PlanValidationError,
    ResearchArtifacts,
    SalesforceAIReportService,
    SalesforcePlan,
    PlanPatch,
    _ensure_patch_summary,
    hash_plan_for_storage,
)
from ...services.salesforce_gateway import SalesforceGateway
from ...services.soql_preflight import preflight_soql_query
from ...services.telemetry import emit
from ...settings import (
    AI_REPORTS_ENABLED,
    AI_REPORTS_MAX_CONCURRENT_GLOBAL,
    AI_REPORTS_MAX_CONCURRENT_PER_ORG,
    AI_REPORTS_MAX_FIELDS,
    AI_REPORTS_MAX_OBJECTS,
    AI_REPORTS_PREVIEW_LIMIT,
    AI_REPORTS_ROW_CAPS,
)
from ...utils.salesforce_errors import normalize_sf_error


router = APIRouter(prefix="/salesforce/ai-reports", tags=["Salesforce AI Reports"])
service = SalesforceAIReportService()
limiter = OrgLimiter(
    AI_REPORTS_MAX_CONCURRENT_PER_ORG,
    AI_REPORTS_MAX_CONCURRENT_GLOBAL,
)
_report_save_validation_cache = HybridCache(
    namespace="ai_reports_save_preflight",
    lru_size=1024,
    ttl=1800,
)


class ConversationTurn(BaseModel):
    model_config = ConfigDict(extra="forbid")

    role: Literal["user", "assistant"] = "user"
    content: str

    @field_validator("role", mode="before")
    @classmethod
    def _normalize_role(cls, value: Any) -> str:
        if value is None:
            return "user"
        normalized = str(value).strip().lower()
        return normalized if normalized in {"user", "assistant"} else "user"


class PlanRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    prompt: str = Field(..., min_length=1, max_length=10000)
    conversation: List[ConversationTurn] = Field(default_factory=list, max_length=100)
    prior_warnings: List[str] = Field(default_factory=list)
    clarifier_answers: Dict[str, str] = Field(
        default_factory=dict, alias="clarifierAnswers"
    )
    discover: Optional[Dict[str, Any]] = None


class DiscoverRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    prompt: str = ""


class PatchRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    plan: Dict[str, Any]
    patch: Dict[str, Any]
    prompt: Optional[str] = None


class PreviewRequest(BaseModel):
    model_config = ConfigDict(extra="forbid", populate_by_name=True)

    plan: Dict[str, Any]
    # tier_override removed - tier is now enforced from authenticated context only
    # This prevents privilege escalation where users could claim higher tiers


class SaveReportRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    name: str = Field(..., min_length=1, max_length=160)  # Match DB constraint
    description: Optional[str] = None
    plan: Dict[str, Any]
    compiled_soql: Optional[str] = None
    scopes: List[str] = Field(default_factory=list)
    schedule: Optional[Dict[str, Any]] = None


class ReportResponse(BaseModel):
    model_config = ConfigDict(extra="allow")

    id: str
    name: str
    description: Optional[str]
    primary_object: str
    plan: Dict[str, Any]
    compiled_soql: str
    created_at: str
    updated_at: str
    tier: Optional[str]
    plan_hash: str


async def _get_gateway(account_id: str, db_session) -> SalesforceGateway:
    return await SalesforceGateway.for_tenant(str(account_id), db_session)


async def _validate_compiled_soql_before_save(
    *,
    gateway: SalesforceGateway,
    account_id: str,
    plan_hash: str,
    compiled_soql: str,
) -> None:
    cache_key = f"{account_id}:{plan_hash}"
    cached = await _report_save_validation_cache.get(cache_key)
    if cached and cached.get("ok") is True:
        return

    await preflight_soql_query(
        gateway,
        compiled_soql,
        user="ai-report-save-preflight",
        ip="0.0.0.0",
        allow_no_limit=True,
    )
    await _report_save_validation_cache.set(cache_key, {"ok": True, "ts_ms": int(time.time() * 1000)})


@router.get("/config")
async def fetch_config(api_ctx=Depends(require_api_key)):
    """Return AI Reports configuration for the authenticated client."""
    tier = getattr(api_ctx, "tier", "FREE")
    return {
        "enabled": AI_REPORTS_ENABLED,
        "previewLimit": AI_REPORTS_PREVIEW_LIMIT,
        "maxFields": AI_REPORTS_MAX_FIELDS,
        "maxObjects": AI_REPORTS_MAX_OBJECTS,
        "rowCap": AI_REPORTS_ROW_CAPS.get(tier, 5000),  # Tier-specific limit
    }


@router.get("/schema")
async def fetch_schema_context(api_ctx=Depends(require_api_key)):
    account_id = api_ctx.account_id
    async for db in get_session():
        gateway = await _get_gateway(account_id, db)
        context = await service.build_schema_context(
            account_id=account_id,
            gateway=gateway,
            prompt="",
            db_session=db,
        )
        return {"ok": True, "data": context.to_payload()}


@router.get("/schema/peek")
async def peek_schema_field(
    object: str, field: str, limit: int = 3, api_ctx=Depends(require_api_key)
):
    if not object or not field:
        raise HTTPException(400, detail="object and field parameters are required")
    account_id = api_ctx.account_id
    async for db in get_session():
        gateway = await _get_gateway(account_id, db)
        try:
            payload = await service.peek_schema_field(
                account_id=account_id,
                gateway=gateway,
                object_name=object,
                field_name=field,
                sample_limit=limit,
            )
        except PlanValidationError as exc:
            raise HTTPException(400, detail=str(exc)) from exc
        return {"ok": True, "data": payload}


@router.post("/discover")
async def discover_context(
    body: DiscoverRequest, request: Request, api_ctx=Depends(require_api_key)
):
    account_id = api_ctx.account_id
    request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
    start = time.perf_counter()
    async for db in get_session():
        gateway = await _get_gateway(account_id, db)
        research, schema_context = await service.discover(
            account_id=account_id,
            prompt=body.prompt,
            gateway=gateway,
            db_session=db,
            request_id=request_id,
        )
        ranked = getattr(research, "ranked_fields", None) or []
        suggested = getattr(research, "suggested_fields", None) or []
        clarifiers = getattr(research, "clarifiers", None) or []
        index_meta = getattr(research, "index_status", None)
        emit(
            "discover.ttfb_ms",
            request_id=request_id,
            org_id=account_id,
            ms=int((time.perf_counter() - start) * 1000),
            suggested=len(ranked) if ranked else len(suggested),
            clarifiers=len(clarifiers),
        )
        return {
            "ok": True,
            "discover": research.model_dump(by_alias=True),
            "schema": schema_context.to_payload(),
            "index_status": index_meta,
            "request_id": request_id,
        }


@router.post("/plan")
async def plan_report(
    body: PlanRequest, request: Request, api_ctx=Depends(require_api_key)
):
    account_id = api_ctx.account_id
    tier = getattr(api_ctx, "tier", None)
    request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
    total_start = time.perf_counter()

    if body.clarifier_answers:
        for clarifier_id, choice in body.clarifier_answers.items():
            emit(
                "clarifier.picked",
                request_id=request_id,
                org_id=account_id,
                clarifier_id=clarifier_id,
                choice=choice,
            )

    async for db in get_session():
        gateway = await _get_gateway(account_id, db)
        try:
            research_override: Optional[ResearchArtifacts] = None
            if body.discover:
                try:
                    research_override = ResearchArtifacts.model_validate(body.discover)
                except ValidationError:
                    research_override = None
            schema_start = time.perf_counter()
            schema_context = await service.build_schema_context(
                account_id=account_id,
                gateway=gateway,
                prompt=body.prompt,
                db_session=db,
            )
            schema_ms = int((time.perf_counter() - schema_start) * 1000)
            plan_start = time.perf_counter()
            artifacts = await service.plan_query(
                account_id=account_id,
                tier=tier,
                prompt=body.prompt,
                gateway=gateway,
                prior_warnings=body.prior_warnings,
                conversation=[turn.model_dump() for turn in body.conversation],
                schema_context=schema_context,
                clarifier_answers=body.clarifier_answers,
                research_override=research_override,
                db_session=db,
                request_id=request_id,
            )
            plan_ms = int((time.perf_counter() - plan_start) * 1000)

            row_cap = service.tier_row_cap(tier)
            validate_start = time.perf_counter()
            warnings, join_suggestions = service.validate_plan(
                plan=artifacts.plan,
                schema_context=schema_context,
                row_cap=row_cap,
            )
            validate_ms = int((time.perf_counter() - validate_start) * 1000)

            compile_start = time.perf_counter()
            soql = service.compile_plan(artifacts.plan)
            compile_ms = int((time.perf_counter() - compile_start) * 1000)
            plan_hash = hash_plan_for_storage(artifacts.plan)
            clarifiers_payload = [
                clarifier.model_dump(by_alias=True)
                for clarifier in artifacts.clarifiers
            ]
            # Include join suggestions in alternatives
            all_alternatives = list(artifacts.alternatives) + join_suggestions
            alternatives_payload = [
                alt.model_dump(by_alias=True) for alt in all_alternatives
            ]
            for alt in alternatives_payload:
                alt.setdefault("patchSummary", alt.get("patch", {}).get("summary"))
            discover_payload = (
                artifacts.research.model_dump(by_alias=True)
                if artifacts.research
                else None
            )
            total_ms = int((time.perf_counter() - total_start) * 1000)
            emit(
                "ai_reports.plan.response_ms",
                request_id=request_id,
                org_id=account_id,
                tier=tier,
                schema_ms=schema_ms,
                plan_ms=plan_ms,
                validate_ms=validate_ms,
                compile_ms=compile_ms,
                total_ms=total_ms,
            )

            return {
                "ok": True,
                "plan": artifacts.plan.model_dump(by_alias=True),
                "compiled_soql": soql,
                "trace": artifacts.trace,
                "clarifiers": clarifiers_payload,
                "risks": artifacts.risks,
                "alternatives": alternatives_payload,
                "discover": discover_payload,
                "warnings": warnings,
                "plan_hash": plan_hash,
                "preview_limit": AI_REPORTS_PREVIEW_LIMIT,
                "row_cap": row_cap,
                "request_id": request_id,
            }
        except PlannerUnavailable as exc:
            raise HTTPException(503, detail=str(exc)) from exc
        except PlanValidationError as exc:
            raise HTTPException(400, detail=str(exc)) from exc


@router.post("/patch")
async def patch_plan(body: PatchRequest, api_ctx=Depends(require_api_key)):
    account_id = api_ctx.account_id
    tier = getattr(api_ctx, "tier", None)

    try:
        base_plan = SalesforcePlan.model_validate(body.plan)
    except ValidationError as exc:
        raise HTTPException(400, detail=f"Invalid plan payload: {exc}") from exc

    try:
        plan_patch = PlanPatch.model_validate(body.patch)
    except ValidationError as exc:
        raise HTTPException(400, detail=f"Invalid patch payload: {exc}") from exc

    summary = _ensure_patch_summary(plan_patch).summary

    async for db in get_session():
        gateway = await _get_gateway(account_id, db)
        try:
            updated_plan = service.apply_patch(base_plan, plan_patch)
            schema_context = await service.build_schema_context(
                account_id=account_id,
                gateway=gateway,
                prompt=body.prompt
                or (updated_plan.objects[0] if updated_plan.objects else ""),
                db_session=db,
            )
            warnings, join_suggestions = service.validate_plan(
                plan=updated_plan,
                schema_context=schema_context,
                row_cap=service.tier_row_cap(tier),
            )
        except PlanValidationError as exc:
            raise HTTPException(400, detail=str(exc)) from exc

        soql = service.compile_plan(updated_plan)
        plan_hash = hash_plan_for_storage(updated_plan)
        # Include join suggestions as auto_fixes in response
        auto_fixes = [alt.model_dump(by_alias=True) for alt in join_suggestions]
        return {
            "ok": True,
            "plan": updated_plan.model_dump(by_alias=True),
            "compiled_soql": soql,
            "warnings": warnings,
            "auto_fixes": auto_fixes,
            "patch_summary": summary,
            "plan_hash": plan_hash,
        }


@router.post("/preview")
async def preview_plan(
    body: PreviewRequest, request: Request, api_ctx=Depends(require_api_key)
):
    account_id = api_ctx.account_id
    tier = getattr(api_ctx, "tier", "FREE")  # Enforce tier from auth context only
    request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())

    try:
        plan = SalesforcePlan.model_validate(body.plan)
    except ValidationError as exc:
        raise HTTPException(400, detail=f"Invalid plan payload: {exc}") from exc

    limiter.acquire(account_id)
    try:
        async for db in get_session():
            gateway = await _get_gateway(account_id, db)
            try:
                focus_prompt = plan.objects[0] if plan.objects else ""
                schema_context = await service.build_schema_context(
                    account_id=account_id,
                    gateway=gateway,
                    prompt=focus_prompt,
                    db_session=db,
                )
                warnings, join_suggestions = service.validate_plan(
                    plan=plan,
                    schema_context=schema_context,
                    row_cap=service.tier_row_cap(tier),
                )
                preview = await service.preview_plan_with_repair(
                    plan=plan,
                    gateway=gateway,
                    account_id=account_id,
                    tier=tier,
                    schema_context=schema_context,
                    request_id=request_id,
                )
                auto_fixes = [alt.model_dump(by_alias=True) for alt in join_suggestions]
                return {
                    "ok": True,
                    "preview": preview,
                    "warnings": warnings,
                    "auto_fixes": auto_fixes,
                    "request_id": request_id,
                }
            except PlanValidationError as exc:
                raise HTTPException(400, detail=str(exc)) from exc
            except Exception as exc:
                payload = getattr(exc, "payload", None)
                if not payload and hasattr(exc, "invalid_fields"):
                    payload = {
                        "errorCode": "INSUFFICIENT_ACCESS",
                        "message": ", ".join(getattr(exc, "invalid_fields", [])),
                    }
                normalized = normalize_sf_error(
                    payload
                    or {"message": str(exc), "error": getattr(exc, "code", "UNKNOWN")}
                )
                status_code = 429 if normalized.kind == "rate_limit" else 400
                detail = normalized.model_dump()
                suggestions = service.suggest_autofix(
                    plan=plan, error_message=normalized.message
                )
                if suggestions:
                    autofix_payload = []
                    for suggestion in suggestions:
                        data = suggestion.model_dump(by_alias=True)
                        data.setdefault(
                            "patchSummary", data.get("patch", {}).get("summary")
                        )
                        autofix_payload.append(data)
                    detail["autofix"] = autofix_payload
                raise HTTPException(status_code=status_code, detail=detail) from exc
    finally:
        limiter.release(account_id)


@router.post("/reports")
async def save_report(body: SaveReportRequest, api_ctx=Depends(require_api_key)):
    account_id = api_ctx.account_id
    user_id = getattr(api_ctx, "api_key_prefix", "api")
    tier = getattr(api_ctx, "tier", None)

    try:
        plan = SalesforcePlan.model_validate(body.plan)
    except ValidationError as exc:
        raise HTTPException(400, detail=f"Invalid plan payload: {exc}") from exc

    if not body.name.strip():
        raise HTTPException(400, detail="Name is required")

    async for db in get_session():
        gateway = await _get_gateway(account_id, db)
        try:
            schema_context = await service.build_schema_context(
                account_id=account_id,
                gateway=gateway,
                prompt=plan.objects[0] if plan.objects else "",
                db_session=db,
            )
            # Just validate, ignore warnings and suggestions for save endpoint
            _ = service.validate_plan(
                plan=plan,
                schema_context=schema_context,
                row_cap=service.tier_row_cap(tier),
            )
        except PlanValidationError as exc:
            raise HTTPException(400, detail=str(exc)) from exc

        repo = SalesforceAIReportsRepo(db)
        plan_hash = hash_plan_for_storage(plan)
        compiled_soql = service.compile_plan(plan)
        await _validate_compiled_soql_before_save(
            gateway=gateway,
            account_id=str(account_id),
            plan_hash=plan_hash,
            compiled_soql=compiled_soql,
        )
        record = await repo.create_report(
            account_id=account_id,
            user_id=user_id,
            name=body.name,
            description=body.description,
            primary_object=plan.objects[0] if plan.objects else "Lead",
            plan_json=plan.model_dump(by_alias=True),
            compiled_soql=compiled_soql,
            plan_hash=plan_hash,
            tier=tier,
            scopes=body.scopes,
            schedule=body.schedule,
        )
        return {
            "ok": True,
            "report": ReportResponse(
                id=record.id,
                name=record.name,
                description=record.description,
                primary_object=record.primary_object,
                plan=body.plan,
                compiled_soql=compiled_soql,
                created_at=record.created_at.isoformat(),
                updated_at=record.updated_at.isoformat(),
                tier=record.tier,
                plan_hash=plan_hash,
            ).model_dump(),
        }


@router.get("/reports")
async def list_reports(api_ctx=Depends(require_api_key)):
    account_id = api_ctx.account_id

    async for db in get_session():
        repo = SalesforceAIReportsRepo(db)
        rows = await repo.list_reports(account_id=account_id)
        out: List[Dict[str, Any]] = []
        for row in rows:
            try:
                plan_payload = json.loads(row.plan_json)
            except Exception:
                plan_payload = {}
            out.append(
                {
                    "id": row.id,
                    "name": row.name,
                    "description": row.description,
                    "primary_object": row.primary_object,
                    "plan": plan_payload,
                    "compiled_soql": row.compiled_soql,
                    "created_at": row.created_at.isoformat(),
                    "updated_at": row.updated_at.isoformat(),
                    "tier": row.tier,
                    "plan_hash": row.plan_hash,
                }
            )
        return {"ok": True, "reports": out}
