"""Flow Builder API endpoints (v2)."""

from __future__ import annotations

import logging
import re
from datetime import datetime, timezone
from typing import List, Optional

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession

from ...auth_security import require_account, require_user
from ...db import get_session
from ...services.flow_architect import FlowArchitect
from ...services.flow_coach import FlowCoach
from ... import settings as saas_settings
from ...services.flow_compiler import FlowCompiler
from ...services.flow_deploy_ticket import FlowDeployTicketStore
from ...services.flow_deployer import FlowDeployer
from ...services.flow_scout import FlowScout
from ...services.flow_sheriff import FlowSheriff
from ...services.salesforce_gateway import SalesforceGateway, get_salesforce_gateway
from ...services.usage_gateway import UsageGateway
from .auth_helpers import resolve_canonical_account_id
from .flow_builder_schemas import (
    ArchitectRequest,
    ArchitectResponse,
    CoachAction,
    CoachBranch,
    CoachDraft,
    CoachRequest,
    CoachResponse,
    CompileRequest,
    CompileResponse,
    ConditionOperator,
    DeployRequest,
    DeployResponse,
    DeployStatusRequest,
    DeployStatusResponse,
    FlowAudit,
    FlowAction,
    FlowAssignmentValue,
    FlowBranch,
    FlowCondition,
    FlowEntryCriteria,
    ScoutRequest,
    ScoutResponse,
    ScoutContext,
    FlowTrigger,
    FlowTriggerType,
    TriggerEvent,
    ValidateRequest,
    ValidateResponse,
    ValidationIssue,
    ValidationResult,
    FlowPlan,
)

router = APIRouter(prefix="/api/v2/flow-builder", tags=["FlowBuilder"])
log = logging.getLogger(__name__)
_FLOW_BUILDER_BETA_ROUTE_ERROR = "FLOW_BUILDER_BETA_ROUTE_DISABLED"


def _flow_builder_upgrade_url() -> str:
    base = (
        getattr(saas_settings, "PUBLIC_BASE_URL", None)
        or saas_settings.APP_URL
        or ""
    ).strip()
    base_lower = base.lower()
    if (
        not base
        or "localhost" in base_lower
        or "127.0.0.1" in base_lower
        or "[::1]" in base_lower
    ):
        return "https://foundryops.io/pricing"
    return f"{base.rstrip('/')}/pricing"


def _raise_beta_route_disabled(route_label: str) -> None:
    raise HTTPException(
        status_code=status.HTTP_410_GONE,
        detail={
            "error": _FLOW_BUILDER_BETA_ROUTE_ERROR,
            "message": (
                f"{route_label} is disabled for the current beta. "
                "Use compile followed by deploy/start."
            ),
        },
    )


def _merge_compile_audit(
    *,
    server_audit: Optional[FlowAudit],
    client_audit: Optional[FlowAudit],
) -> Optional[FlowAudit]:
    # The server audit is authoritative, but we keep any stricter
    # acknowledgment requirement the client already surfaced so a stale
    # sidebar cannot silently relax a previously-reviewed critical warning.
    if server_audit is None:
        return client_audit
    if client_audit is None:
        return server_audit
    if (
        client_audit.requires_acknowledgment
        and not server_audit.requires_acknowledgment
    ):
        return server_audit.model_copy(update={"requires_acknowledgment": True})
    return server_audit


async def _resolve_canonical_account_id(
    account_id: str,
    db: AsyncSession,
) -> Optional[str]:
    canonical_account_id = await resolve_canonical_account_id(account_id, db)
    if canonical_account_id:
        return canonical_account_id
    log.debug("Flow Builder account canonicalization lookup failed for %s", account_id)
    return None


async def _resolve_paid_tier(account_id: str, db: AsyncSession) -> str:
    """
    Resolve paid tier using UsageGateway first (canonical source), then fallback.
    """
    canonical_account_id = await _resolve_canonical_account_id(account_id, db)
    if canonical_account_id:
        try:
            capabilities = await UsageGateway(db).get_capabilities(canonical_account_id)
            tier = str((capabilities or {}).get("tier") or "").strip().lower()
            if tier:
                return tier
        except Exception:
            log.exception(
                "Flow Builder tier lookup via UsageGateway failed for account %s",
                canonical_account_id,
            )

    # Import here to avoid circular dependency
    from .gremlin import get_gremlin_tier

    fallback_account_id = canonical_account_id or account_id
    return (await get_gremlin_tier(fallback_account_id, db)).strip().lower()


def _raise_llm_guard_errors(raw: Optional[dict]) -> None:
    if not raw:
        return
    err_type = raw.get("_error_type")
    retry_after = raw.get("_retry_after_s")
    if err_type == "LLMConcurrencyError":
        retry_after_s = int(retry_after or 30)
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail={
                "error": "LLM_CONCURRENCY",
                "message": "Too many requests right now. Please try again in 30 seconds.",
                "retry_after_s": retry_after_s,
            },
            headers={"Retry-After": str(retry_after_s)},
        )
    if err_type == "LLMCircuitOpenError":
        retry_after_s = int(retry_after or 60)
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail={
                "error": "LLM_CIRCUIT_OPEN",
                "message": "Service temporarily unavailable. Please try again shortly.",
                "retry_after_s": retry_after_s,
            },
            headers={"Retry-After": str(retry_after_s)},
        )


async def require_paid_tier(account_id: str, db: AsyncSession) -> None:
    """
    Enforce paid tier access for Flow Builder.

    Raises HTTPException 402 if user is on free tier.
    Same behavior as AI SFDC Report Builder.
    """
    tier = await _resolve_paid_tier(account_id, db)
    if tier == "free":
        upgrade_url = _flow_builder_upgrade_url()
        raise HTTPException(
            status_code=status.HTTP_402_PAYMENT_REQUIRED,
            detail={
                "error": "UPGRADE_REQUIRED",
                "message": "Flow Builder requires a paid tier.",
                "upgrade_url": upgrade_url,
            },
        )


class SessionRequest(BaseModel):
    prompt: str = Field(..., min_length=10, max_length=2000)
    object_api_name: str
    record_type_id: Optional[str] = None
    include_validation_rules: bool = True
    include_existing_flows: bool = True
    run_critic: bool = True


class SessionResponse(BaseModel):
    success: bool
    context: Optional[ScoutContext] = None
    plan: Optional[FlowPlan] = None
    validation: Optional[ValidationResult] = None
    audit: Optional[FlowAudit] = None
    xml: Optional[str] = None
    deploy: Optional[DeployResponse] = None
    error: Optional[str] = None


class CoachPlanRequest(BaseModel):
    draft: CoachDraft
    context: ScoutContext
    object_api_name: Optional[str] = None
    record_type_id: Optional[str] = None
    flow_name: Optional[str] = None
    description: Optional[str] = None


def _slug_token(value: Optional[str], fallback: str) -> str:
    token = re.sub(r"[^A-Za-z0-9]+", "_", value or "").strip("_")
    if not token:
        token = fallback or "Token"
    if not token[0].isalpha():
        token = f"F_{token}"
    return token


def _draft_primary_action_field(draft: CoachDraft) -> Optional[str]:
    if draft.actions:
        return draft.actions[0].field
    if draft.branches:
        for branch in draft.branches:
            if branch.actions:
                return branch.actions[0].field
    return None


def _derive_flow_name(
    draft: CoachDraft,
    object_api_name: str,
    explicit_name: Optional[str] = None,
) -> str:
    if explicit_name:
        return explicit_name

    ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
    object_token = _slug_token(object_api_name, "Object")
    field_token = _slug_token(_draft_primary_action_field(draft), "Field")
    base = f"FO_{object_token}_{field_token}_Coach"

    candidate = f"{base}_{ts}"
    if len(candidate) <= 80:
        return candidate

    keep = max(10, 80 - len(ts) - 1)
    base = base[:keep].rstrip("_")
    return f"{base}_{ts}"


def _coach_action_to_flow(action: CoachAction) -> FlowAction:
    source = "field_reference" if action.value_is_field else "literal"
    return FlowAction(
        type="assignment",
        field=action.field,
        value=FlowAssignmentValue(
            source=source,
            content=action.value,
        ),
    )


def _copy_conditions(conditions: List[FlowCondition]) -> List[FlowCondition]:
    return [
        FlowCondition(
            field=cond.field,
            operator=cond.operator,
            value=cond.value,
            value_is_field=cond.value_is_field,
        )
        for cond in (conditions or [])
    ]


def _has_is_null_guard(conditions: List[FlowCondition], field_name: str) -> bool:
    return any(
        cond.field == field_name and cond.operator == ConditionOperator.IS_NULL
        for cond in conditions
    )


def _add_blank_guards(
    base_conditions: List[FlowCondition],
    actions: List[CoachAction],
) -> List[FlowCondition]:
    merged = _copy_conditions(base_conditions)
    for action in actions or []:
        if not action.blank_only:
            continue
        if _has_is_null_guard(merged, action.field):
            continue
        merged.append(
            FlowCondition(
                field=action.field,
                operator=ConditionOperator.IS_NULL,
                value=None,
                value_is_field=False,
            )
        )
    return merged


def _coach_branch_to_flow(branch: CoachBranch) -> FlowBranch:
    branch_actions = [_coach_action_to_flow(action) for action in branch.actions or []]
    branch_conditions = _add_blank_guards(branch.conditions or [], branch.actions or [])
    return FlowBranch(
        name=branch.name,
        logic=branch.logic,
        conditions=branch_conditions,
        actions=branch_actions,
    )


def _derive_description(
    draft: CoachDraft,
    object_api_name: str,
    explicit_description: Optional[str] = None,
) -> str:
    if explicit_description:
        return explicit_description
    if draft.branches:
        return (
            f"Coach-generated flow for {object_api_name} "
            f"with {len(draft.branches)} branch(es)."
        )
    action_count = len(draft.actions or [])
    return (
        f"Coach-generated flow for {object_api_name} "
        f"with {action_count} assignment(s)."
    )


def _coach_draft_to_plan(
    *,
    draft: CoachDraft,
    object_api_name: str,
    record_type_id: Optional[str],
    flow_name: Optional[str],
    description: Optional[str],
) -> FlowPlan:
    events = draft.events or [TriggerEvent.CREATE]
    effective_record_type_id = record_type_id or draft.record_type_id
    trigger = FlowTrigger(
        object_api_name=object_api_name,
        type=FlowTriggerType.RECORD_BEFORE_SAVE,
        events=events,
        record_type_id=effective_record_type_id,
    )

    if draft.branches:
        branches = [_coach_branch_to_flow(branch) for branch in draft.branches]
        global_conditions = _copy_conditions(draft.conditions or [])
        entry_criteria = (
            FlowEntryCriteria(logic=draft.logic, conditions=global_conditions)
            if global_conditions
            else None
        )
        actions: List[FlowAction] = []
    else:
        actions = [_coach_action_to_flow(action) for action in draft.actions or []]
        guarded_conditions = _add_blank_guards(draft.conditions or [], draft.actions or [])
        entry_criteria = (
            FlowEntryCriteria(logic=draft.logic, conditions=guarded_conditions)
            if guarded_conditions
            else None
        )
        branches = []

    return FlowPlan(
        flow_name=_derive_flow_name(
            draft=draft,
            object_api_name=object_api_name,
            explicit_name=flow_name,
        ),
        description=_derive_description(
            draft=draft,
            object_api_name=object_api_name,
            explicit_description=description,
        ),
        trigger=trigger,
        entry_criteria=entry_criteria,
        actions=actions,
        branches=branches,
        user_prompt="Generated from coach draft",
    )


@router.post("/scout", response_model=ScoutResponse)
async def scout(
    payload: ScoutRequest,
    _: str = Depends(require_user),
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
    gateway: SalesforceGateway = Depends(get_salesforce_gateway),
):
    await require_paid_tier(account_id, db)
    try:
        scout = FlowScout(gateway)
        context = await scout.fetch_context(
            payload.object_api_name,
            include_validation_rules=payload.include_validation_rules,
            include_existing_flows=payload.include_existing_flows,
        )
        return ScoutResponse(
            success=True, context=context, fls_warnings=context.fls_warnings
        )
    except HTTPException:
        raise
    except Exception as exc:
        log.exception("FlowBuilder scout failed: %s", exc)
        return ScoutResponse(success=False, error=str(exc))


@router.post("/architect", response_model=ArchitectResponse)
async def architect(
    payload: ArchitectRequest,
    _: str = Depends(require_user),
    account_id: str = Depends(require_account),
    gateway: SalesforceGateway = Depends(get_salesforce_gateway),
    db: AsyncSession = Depends(get_session),
):
    # Enforce paid tier
    await require_paid_tier(account_id, db)

    try:
        context = payload.context
        if context is None:
            scout = FlowScout(gateway)
            context = await scout.fetch_context(
                payload.object_api_name,
                include_validation_rules=True,
                include_existing_flows=True,
            )

        architect = FlowArchitect()
        result = architect.build_plan(
            prompt=payload.prompt,
            object_api_name=payload.object_api_name,
            context=context,
            record_type_id=payload.record_type_id,
            run_critic=True,
            job_id=payload.job_id,
            account_id=account_id,
        )
        _raise_llm_guard_errors(result.raw)
        if result.plan is None:
            return ArchitectResponse(
                success=False,
                error=result.error or "Architect failed",
                model_used=result.model_used,
            )

        validation = FlowSheriff(context).validate(result.plan)
        return ArchitectResponse(
            success=True,
            plan=result.plan,
            validation=validation,
            audit=result.audit,
            model_used=result.model_used,
        )
    except HTTPException:
        raise
    except Exception as exc:
        log.exception("FlowBuilder architect failed: %s", exc)
        return ArchitectResponse(success=False, error=str(exc))


@router.post("/coach", response_model=CoachResponse)
async def coach(
    payload: CoachRequest,
    _: str = Depends(require_user),
    account_id: str = Depends(require_account),
    gateway: SalesforceGateway = Depends(get_salesforce_gateway),
    db: AsyncSession = Depends(get_session),
):
    await require_paid_tier(account_id, db)

    try:
        object_api_name = payload.object_api_name or (
            payload.draft.object_api_name if payload.draft else None
        )
        if not object_api_name:
            return CoachResponse(
                success=False,
                needs_more_info=True,
                questions=[],
                draft=payload.draft,
                error="object_api_name is required",
            )

        context = payload.context
        if context is None:
            scout = FlowScout(gateway)
            context = await scout.fetch_context(
                object_api_name,
                include_validation_rules=True,
                include_existing_flows=True,
            )

        coach = FlowCoach()
        result = coach.coach(
            message=payload.message,
            history=payload.history,
            draft=payload.draft,
            context=context,
            object_api_name=object_api_name,
            record_type_id=payload.record_type_id,
            conversation_id=payload.conversation_id,
            account_id=account_id,
        )
        _raise_llm_guard_errors(result.raw)
        return result.response
    except HTTPException:
        raise
    except Exception as exc:
        log.exception("FlowBuilder coach failed: %s", exc)
        return CoachResponse(
            success=False,
            needs_more_info=True,
            questions=[],
            draft=payload.draft,
            error=str(exc),
        )


@router.post("/coach/plan", response_model=ArchitectResponse)
async def coach_plan(
    payload: CoachPlanRequest,
    _: str = Depends(require_user),
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
):
    # Keep parity with architect/compile/deploy access controls.
    await require_paid_tier(account_id, db)

    try:
        object_api_name = payload.object_api_name or payload.draft.object_api_name
        if not object_api_name:
            return ArchitectResponse(
                success=False,
                error="object_api_name is required",
                model_used="coach_draft",
            )

        plan = _coach_draft_to_plan(
            draft=payload.draft,
            object_api_name=object_api_name,
            record_type_id=payload.record_type_id,
            flow_name=payload.flow_name,
            description=payload.description,
        )
        validation = FlowSheriff(payload.context).validate(plan)
        audit = FlowArchitect().audit_plan(
            plan,
            payload.context,
            account_id=account_id,
        )

        return ArchitectResponse(
            success=True,
            plan=plan,
            validation=validation,
            audit=audit,
            model_used="coach_draft",
        )
    except HTTPException:
        raise
    except Exception as exc:
        log.exception("FlowBuilder coach plan failed: %s", exc)
        return ArchitectResponse(
            success=False,
            error=str(exc),
            model_used="coach_draft",
        )


@router.post("/validate", response_model=ValidateResponse)
async def validate(
    payload: ValidateRequest,
    _: str = Depends(require_user),
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
):
    await require_paid_tier(account_id, db)

    try:
        validation = FlowSheriff(payload.context).validate(payload.plan)
        return ValidateResponse(
            success=True,
            validation=validation,
            deployable=validation.deployable,
        )
    except HTTPException:
        raise
    except Exception as exc:
        log.exception("FlowBuilder validate failed: %s", exc)
        return ValidateResponse(
            success=False,
            validation=ValidationResult(
                issues=[
                    ValidationIssue(
                        severity="error",
                        code="VALIDATION_INTERNAL_ERROR",
                        message=str(exc),
                    )
                ]
            ),
            deployable=False,
        )


@router.post("/compile", response_model=CompileResponse)
async def compile_flow(
    payload: CompileRequest,
    _: str = Depends(require_user),
    account_id: str = Depends(require_account),
    db: AsyncSession = Depends(get_session),
):
    # Enforce paid tier
    await require_paid_tier(account_id, db)

    try:
        validation = FlowSheriff(payload.context).validate(payload.plan)
        if not validation.deployable:
            return CompileResponse(
                success=False,
                xml=None,
                flow_api_name=payload.plan.flow_name,
                error="Validation failed",
            )

        server_audit = FlowArchitect().audit_plan(
            payload.plan,
            payload.context,
            account_id=account_id,
        )
        effective_audit = _merge_compile_audit(
            server_audit=server_audit,
            client_audit=payload.audit,
        )
        if (
            effective_audit is not None
            and effective_audit.requires_acknowledgment
            and not payload.audit_acknowledged
        ):
            return CompileResponse(
                success=False,
                xml=None,
                flow_api_name=payload.plan.flow_name,
                audit=effective_audit,
                error=(
                    "Critical risk review must be acknowledged before "
                    "draft deployment."
                ),
            )

        compiler = FlowCompiler()
        xml = compiler.compile(payload.plan, payload.context)
        ticket = FlowDeployTicketStore.issue_ticket(
            account_id=account_id,
            flow_api_name=payload.plan.flow_name,
            flow_xml=xml,
            plan=payload.plan,
            context=payload.context,
            audit=effective_audit,
            audit_acknowledged=payload.audit_acknowledged,
        )
        return CompileResponse(
            success=True,
            xml=xml,
            flow_api_name=payload.plan.flow_name,
            audit=effective_audit,
            deploy_ticket=ticket.ticket_id,
            deploy_ticket_expires_at=ticket.expires_at.isoformat(),
            error=None,
        )
    except HTTPException:
        raise
    except Exception as exc:
        log.exception("FlowBuilder compile failed: %s", exc)
        return CompileResponse(
            success=False,
            xml=None,
            flow_api_name=payload.plan.flow_name,
            error=str(exc),
        )


@router.post("/deploy", response_model=DeployResponse)
async def deploy_flow(
    payload: DeployRequest,
    _: str = Depends(require_user),
    account_id: str = Depends(require_account),
    gateway: SalesforceGateway = Depends(get_salesforce_gateway),
    db: AsyncSession = Depends(get_session),
):
    # Enforce paid tier
    await require_paid_tier(account_id, db)
    _raise_beta_route_disabled("Synchronous deploy")


@router.post("/deploy/start", response_model=DeployResponse)
async def deploy_flow_start(
    payload: DeployRequest,
    _: str = Depends(require_user),
    account_id: str = Depends(require_account),
    gateway: SalesforceGateway = Depends(get_salesforce_gateway),
    db: AsyncSession = Depends(get_session),
):
    # Enforce paid tier
    await require_paid_tier(account_id, db)

    try:
        if not payload.deploy_ticket:
            return DeployResponse(
                success=False,
                deployment_id=None,
                flow_id=None,
                status="failed",
                error="deploy_ticket is required",
                warnings=[],
            )
        ticket, ticket_error = FlowDeployTicketStore.consume_ticket(
            payload.deploy_ticket,
            account_id=account_id,
        )
        if ticket is None:
            error_message = "Deploy ticket is invalid or expired."
            if ticket_error == "account_mismatch":
                error_message = "Deploy ticket does not belong to this account."
            return DeployResponse(
                success=False,
                deployment_id=None,
                flow_id=None,
                status="failed",
                error=error_message,
                warnings=[],
            )
        deployer = FlowDeployer(gateway)
        return await deployer.start_deploy(ticket.flow_xml, ticket.flow_api_name)
    except Exception as exc:
        log.exception("FlowBuilder async deploy start failed: %s", exc)
        return DeployResponse(
            success=False,
            deployment_id=None,
            flow_id=None,
            status="failed",
            error=str(exc),
            warnings=[],
        )


@router.post("/deploy/status", response_model=DeployStatusResponse)
async def deploy_flow_status(
    payload: DeployStatusRequest,
    _: str = Depends(require_user),
    account_id: str = Depends(require_account),
    gateway: SalesforceGateway = Depends(get_salesforce_gateway),
    db: AsyncSession = Depends(get_session),
):
    # Enforce paid tier
    await require_paid_tier(account_id, db)

    try:
        status = await gateway.check_deploy_status(payload.deployment_id)
        if not isinstance(status, dict):
            return DeployStatusResponse(
                success=True,
                deployment_id=payload.deployment_id,
                done=False,
                status="Retrying",
                error="Invalid deployment status response",
                warnings=["Transient deploy status error; retrying."],
            )

        done = bool(status.get("done"))
        success = bool(status.get("success")) if done else False
        return DeployStatusResponse(
            success=success,
            deployment_id=payload.deployment_id,
            done=done,
            status=status.get("status", "InProgress"),
            error=status.get("error_message") if done and not success else None,
            warnings=status.get("errors", []) if done and not success else [],
        )
    except Exception as exc:
        log.exception("FlowBuilder deploy status failed: %s", exc)
        return DeployStatusResponse(
            success=True,
            deployment_id=payload.deployment_id,
            done=False,
            status="Retrying",
            error=str(exc),
            warnings=["Transient deploy status error; retrying."],
        )


@router.post("/session", response_model=SessionResponse)
async def session(
    payload: SessionRequest,
    _: str = Depends(require_user),
    account_id: str = Depends(require_account),
    gateway: SalesforceGateway = Depends(get_salesforce_gateway),
    db: AsyncSession = Depends(get_session),
):
    # Enforce paid tier
    await require_paid_tier(account_id, db)
    _raise_beta_route_disabled("One-shot session deploy")
