from __future__ import annotations

import asyncio
import json
from collections.abc import AsyncIterator
from typing import Annotated, Any

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import StreamingResponse
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext, get_current_user, get_db
from app.core.config import get_settings
from app.models.financial_record import ExpenseClaim
from app.schemas.common import ErrorResponse
from app.schemas.steward import (
    StewardActionExecuteRequest,
    StewardActionExecuteResponse,
    StewardPlanRequest,
    StewardPlanResponse,
    StewardRuntimeDecisionRequest,
    StewardRuntimeDecisionResponse,
    StewardSlotDecisionRequest,
    StewardSlotDecisionResponse,
    StewardThinkingEvent,
)
from app.services.agent_conversations import AgentConversationService
from app.services.expense_claim_draft_flow import APPROVED_APPLICATION_LINK_STATUSES
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin
from app.services.expense_claims import ExpenseClaimService
from app.services.runtime_chat import RuntimeChatService
from app.services.steward_context_resume import (
    attach_resumed_task,
    should_resume_recent_task,
)
from app.services.steward_flow_state import StewardFlowStateService
from app.services.steward_graph_action_runtime import StewardGraphActionRuntime
from app.services.steward_graph_planner import StewardGraphPlannerService
from app.services.steward_graph_runtime import StewardGraphRuntime
from app.services.steward_intent_agent import StewardIntentAgent
from app.services.steward_off_topic_agent import StewardOffTopicAgent
from app.services.steward_planner import StewardPlannerService
from app.services.steward_runtime_decision_agent import StewardRuntimeDecisionAgent
from app.services.steward_slot_decision_agent import StewardSlotDecisionAgent

router = APIRouter(prefix="/steward")
DbSession = Annotated[Session, Depends(get_db)]
CurrentUser = Annotated[CurrentUserContext, Depends(get_current_user)]
StewardPlannerLike = StewardPlannerService | StewardGraphPlannerService


@router.post(
    "/plans",
    response_model=StewardPlanResponse,
    summary="生成小财管家任务计划",
    description="把首页自然语言和附件元信息拆解为可确认、可追踪、可分派的财务任务计划。",
    responses={
        status.HTTP_400_BAD_REQUEST: {
            "model": ErrorResponse,
            "description": "请求缺少任务描述，无法生成小财管家计划。",
        }
    },
)
def create_steward_plan(
    payload: StewardPlanRequest,
    db: DbSession,
    current_user: CurrentUser,
) -> StewardPlanResponse:
    try:
        payload = _bind_authenticated_plan_request(payload, current_user)
        planner = _build_steward_planner(db)
        hydrated_payload = _hydrate_required_application_gate(
            db,
            payload,
            planner,
            tenant_id=_require_steward_tenant_id(current_user),
        )
        if isinstance(planner, StewardGraphPlannerService):
            plan = planner.build_plan(hydrated_payload, db=db)
        else:
            plan = planner.build_plan(hydrated_payload)
        return _attach_conversation_state(
            db,
            hydrated_payload,
            plan,
            current_user=current_user,
        )
    except ValueError as exc:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc


@router.post(
    "/slot-decisions",
    response_model=StewardSlotDecisionResponse,
    summary="判断小财管家当前任务字段缺口",
    description="结合当前任务、本体字段和用户上下文，使用 function calling 判断下一步应先追问用户还是展示核对结果。",
)
def create_steward_slot_decision(
    payload: StewardSlotDecisionRequest,
    db: DbSession,
    current_user: CurrentUser,
) -> StewardSlotDecisionResponse:
    authenticated_payload = _bind_authenticated_slot_request(payload, current_user)
    return _decide_steward_slot(authenticated_payload, RuntimeChatService(db))


@router.post(
    "/runtime-decisions",
    response_model=StewardRuntimeDecisionResponse,
    summary="判断小财管家运行时下一步动作",
    description="结合任务队列、当前结构化结果和用户输入，使用 function calling 判断应提交当前单据、继续下一任务、补字段或重新规划。",
)
def create_steward_runtime_decision(
    payload: StewardRuntimeDecisionRequest,
    db: DbSession,
    current_user: CurrentUser,
) -> StewardRuntimeDecisionResponse:
    authenticated_payload = _bind_authenticated_runtime_request(payload, current_user)
    hydrated_payload = _hydrate_runtime_decision_payload(
        db,
        authenticated_payload,
        current_user=current_user,
    )
    decision = _decide_steward_runtime(hydrated_payload, RuntimeChatService(db))
    return _attach_runtime_conversation_state(
        db,
        hydrated_payload,
        decision,
        current_user=current_user,
    )


@router.post(
    "/actions/execute",
    response_model=StewardActionExecuteResponse,
    summary="执行小财管家白名单动作",
    description=(
        "按 LangGraph 规划出的 action step 执行确定性业务动作；"
        "当 action 未知、缺字段、缺确认或预检查未通过时直接阻断并返回结构化兜底结果。"
    ),
)
def execute_steward_action(
    payload: StewardActionExecuteRequest,
    db: DbSession,
    current_user: CurrentUser,
) -> StewardActionExecuteResponse:
    return StewardGraphActionRuntime(db).execute(payload, current_user)


@router.post(
    "/plans/stream",
    summary="流式生成小财管家任务计划",
    description="以 NDJSON 逐条返回小财管家的过程摘要事件，最后返回完整任务计划。",
)
async def stream_steward_plan(
    payload: StewardPlanRequest,
    db: DbSession,
    current_user: CurrentUser,
) -> StreamingResponse:
    authenticated_payload = _bind_authenticated_plan_request(payload, current_user)
    return StreamingResponse(
        _iter_steward_plan_events(
            authenticated_payload,
            _build_steward_planner(db),
            db,
            current_user=current_user,
        ),
        media_type="application/x-ndjson",
    )


async def _iter_steward_plan_events(
    payload: StewardPlanRequest,
    planner: StewardPlannerLike,
    db: Session,
    *,
    current_user: CurrentUserContext,
) -> AsyncIterator[str]:
    yield _encode_stream_event(
        "thinking",
        StewardThinkingEvent(
            event_id="intent_agent_stream_start",
            stage="stream_start",
            title="读取用户输入",
            content="我先识别申请/报销边界；如果是历史差旅描述，会先查询可关联申请单再决定流程。",
            status="running",
        ).model_dump(mode="json"),
    )
    await asyncio.sleep(0)

    try:
        hydrated_payload = _hydrate_required_application_gate(
            db,
            payload,
            planner,
            tenant_id=_require_steward_tenant_id(current_user),
        )
        if isinstance(planner, StewardGraphPlannerService):
            plan = planner.build_plan(hydrated_payload, db=db)
        else:
            plan = planner.build_plan(hydrated_payload)
        plan = _attach_conversation_state(
            db,
            hydrated_payload,
            plan,
            current_user=current_user,
        )
    except ValueError as exc:
        yield _encode_stream_event("error", {"message": str(exc)})
        return

    for event in plan.thinking_events:
        yield _encode_stream_event("thinking", event.model_dump(mode="json"))
        await asyncio.sleep(0.6)

    yield _encode_stream_event("plan", plan.model_dump(mode="json"))


def _encode_stream_event(event: str, data: dict[str, Any]) -> str:
    return json.dumps({"event": event, "data": data}, ensure_ascii=False) + "\n"


def _bind_authenticated_plan_request(
    payload: StewardPlanRequest,
    current_user: CurrentUserContext,
) -> StewardPlanRequest:
    user_id = _require_steward_user_id(current_user)
    return payload.model_copy(
        update={
            "user_id": user_id,
            "context_json": _build_trusted_steward_context(
                payload.context_json,
                current_user,
            ),
        }
    )


def _bind_authenticated_slot_request(
    payload: StewardSlotDecisionRequest,
    current_user: CurrentUserContext,
) -> StewardSlotDecisionRequest:
    return payload.model_copy(
        update={
            "task_context": _build_trusted_steward_context(
                payload.task_context,
                current_user,
            )
        }
    )


def _bind_authenticated_runtime_request(
    payload: StewardRuntimeDecisionRequest,
    current_user: CurrentUserContext,
) -> StewardRuntimeDecisionRequest:
    return payload.model_copy(
        update={
            "context_json": _build_trusted_steward_context(
                payload.context_json,
                current_user,
            ),
            "runtime_state": _build_trusted_steward_context(
                payload.runtime_state,
                current_user,
            ),
        }
    )


def _build_trusted_steward_context(
    context_json: dict[str, Any] | None,
    current_user: CurrentUserContext,
) -> dict[str, Any]:
    tenant_id = _require_steward_tenant_id(current_user)
    user_id = _require_steward_user_id(current_user)
    trusted_context = dict(context_json or {})
    for untrusted_alias in (
        "tenantId",
        "userId",
        "auth_session_id",
    ):
        trusted_context.pop(untrusted_alias, None)
    trusted_context.update(
        {
            "tenant_id": tenant_id,
            "user_id": user_id,
            "username": current_user.username,
            "name": current_user.name,
            "role_codes": list(current_user.role_codes),
            "is_admin": current_user.is_admin,
            "department": current_user.department_name,
            "department_name": current_user.department_name,
            "department_id": current_user.department_id,
            "cost_center": current_user.cost_center,
            "position": current_user.position,
            "grade": current_user.grade,
            "employee_grade": current_user.grade,
            "employee_no": current_user.employee_no,
            "employee_id": current_user.employee_id,
            "manager_name": current_user.manager_name,
            "requested_by_username": current_user.username,
            "requested_by_name": current_user.name,
            "actor": user_id,
            "actor_id": user_id,
        }
    )
    return trusted_context


def _require_steward_tenant_id(current_user: CurrentUserContext) -> str:
    tenant_id = str(current_user.tenant_id or "").strip()
    if tenant_id:
        return tenant_id
    raise HTTPException(
        status_code=status.HTTP_403_FORBIDDEN,
        detail="当前登录用户缺少租户归属，无法使用小财管家。",
    )


def _require_steward_user_id(current_user: CurrentUserContext) -> str:
    user_id = str(current_user.username or current_user.employee_id or "").strip()
    if user_id:
        return user_id
    raise HTTPException(
        status_code=status.HTTP_403_FORBIDDEN,
        detail="当前登录用户缺少可信用户标识，无法使用小财管家。",
    )


def _build_steward_planner(db: Session) -> StewardPlannerLike:
    runtime_chat = RuntimeChatService(db)
    if get_settings().steward_agent_runtime.strip().lower() == "langgraph":
        return StewardGraphPlannerService(
            intent_agent=StewardIntentAgent(runtime_chat),
            off_topic_agent=StewardOffTopicAgent(runtime_chat),
        )
    return StewardPlannerService(
        intent_agent=StewardIntentAgent(runtime_chat),
        off_topic_agent=StewardOffTopicAgent(runtime_chat),
    )


def _decide_steward_slot(
    payload: StewardSlotDecisionRequest,
    runtime_chat: Any,
) -> StewardSlotDecisionResponse:
    legacy_agent = StewardSlotDecisionAgent(runtime_chat)
    if not _should_use_langgraph_runtime():
        return legacy_agent.decide(payload)
    try:
        return StewardGraphRuntime(runtime_chat).decide_slot(payload)
    except Exception:
        return legacy_agent.decide(payload)


def _decide_steward_runtime(
    payload: StewardRuntimeDecisionRequest,
    runtime_chat: Any,
) -> StewardRuntimeDecisionResponse:
    legacy_agent = StewardRuntimeDecisionAgent(runtime_chat)
    if not _should_use_langgraph_runtime():
        return legacy_agent.decide(payload)
    try:
        return StewardGraphRuntime(runtime_chat).decide_runtime(payload)
    except Exception:
        return legacy_agent.decide(payload)


def _should_use_langgraph_runtime() -> bool:
    return get_settings().steward_agent_runtime.strip().lower() == "langgraph"


def _hydrate_required_application_gate(
    db: Session,
    payload: StewardPlanRequest,
    planner: StewardPlannerLike,
    *,
    tenant_id: str,
) -> StewardPlanRequest:
    context_json = dict(payload.context_json or {})
    required_gate = context_json.get("required_application_gate")
    if isinstance(required_gate, dict):
        travel_gate = required_gate.get("travel")
        if isinstance(travel_gate, dict) and travel_gate.get("checked") is True:
            return payload

    message = planner._clean_text(payload.message)
    base_date = planner._resolve_base_date(payload.client_now_iso, context_json)
    if not planner._looks_like_ambiguous_travel_flow(message, base_date, payload):
        return payload

    candidates = _query_required_application_gate_candidates(
        db,
        payload,
        context_json,
        tenant_id=tenant_id,
    )
    next_required_gate = dict(required_gate) if isinstance(required_gate, dict) else {}
    next_required_gate["travel"] = {
        "checked": True,
        "candidate_count": len(candidates),
        "candidates": candidates[:5],
    }
    return payload.model_copy(
        update={
            "context_json": {
                **context_json,
                "required_application_gate": next_required_gate,
            }
        }
    )


def _query_required_application_gate_candidates(
    db: Session,
    payload: StewardPlanRequest,
    context_json: dict[str, Any],
    *,
    tenant_id: str,
) -> list[dict[str, Any]]:
    identities = _resolve_required_application_gate_identities(payload, context_json)
    stmt = (
        select(ExpenseClaim)
        .where(
            ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(tenant_id)
        )
        .order_by(ExpenseClaim.submitted_at.desc(), ExpenseClaim.updated_at.desc())
        .limit(200)
    )
    candidates: list[dict[str, Any]] = []
    for claim in db.scalars(stmt).all():
        if not ExpenseClaimService._is_expense_application_claim(claim):
            continue
        if str(claim.status or "").strip().lower() not in APPROVED_APPLICATION_LINK_STATUSES:
            continue
        if identities and not _claim_matches_required_application_identity(claim, identities):
            continue
        if not _claim_matches_required_travel_application(claim, payload.message):
            continue
        candidates.append(_serialize_required_application_gate_candidate(claim))
    return candidates


def _resolve_required_application_gate_identities(
    payload: StewardPlanRequest,
    context_json: dict[str, Any],
) -> set[str]:
    raw_values = [
        payload.user_id,
        context_json.get("user_id"),
        context_json.get("username"),
        context_json.get("name"),
        context_json.get("employee_id"),
        context_json.get("employee_no"),
        context_json.get("employee_name"),
    ]
    identities: set[str] = set()
    for value in raw_values:
        normalized = _normalize_required_application_identity(value)
        if normalized:
            identities.add(normalized)
    return identities


def _normalize_required_application_identity(value: Any) -> str:
    return str(value or "").strip().casefold()


def _claim_matches_required_application_identity(claim: ExpenseClaim, identities: set[str]) -> bool:
    claim_identities = {
        _normalize_required_application_identity(claim.employee_id),
        _normalize_required_application_identity(claim.employee_name),
    }
    claim_identities.discard("")
    return bool(claim_identities.intersection(identities))


def _claim_matches_required_travel_application(claim: ExpenseClaim, message: str) -> bool:
    expense_type = str(claim.expense_type or "").strip().casefold()
    if any(token in expense_type for token in ("travel", "trip", "差旅", "出差")):
        return True

    claim_text = "".join(
        [
            str(claim.reason or ""),
            str(claim.location or ""),
            str(claim.claim_no or ""),
        ]
    )
    if "差旅" in claim_text or "出差" in claim_text:
        return True

    compact_message = str(message or "").replace(" ", "")
    location = str(claim.location or "").strip()
    return bool(location and location in compact_message and "出差" in compact_message)


def _serialize_required_application_gate_candidate(claim: ExpenseClaim) -> dict[str, Any]:
    business_time = _resolve_required_application_business_time(claim)
    status_label = _resolve_required_application_status_label(claim.status)
    return {
        "id": str(claim.id or "").strip(),
        "claim_no": str(claim.claim_no or "").strip(),
        "reason": str(claim.reason or "").strip(),
        "location": str(claim.location or "").strip(),
        "business_time": business_time,
        "status_label": status_label,
        "application_claim_id": str(claim.id or "").strip(),
        "application_claim_no": str(claim.claim_no or "").strip(),
        "application_reason": str(claim.reason or "").strip(),
        "application_location": str(claim.location or "").strip(),
        "application_business_time": business_time,
        "application_status_label": status_label,
    }


def _resolve_required_application_business_time(claim: ExpenseClaim) -> str:
    for flag in list(claim.risk_flags_json or []):
        if not isinstance(flag, dict):
            continue
        for source in (
            flag,
            flag.get("application_detail"),
            flag.get("applicationDetail"),
            flag.get("review_form_values"),
            flag.get("reviewFormValues"),
        ):
            if not isinstance(source, dict):
                continue
            value = (
                source.get("application_business_time")
                or source.get("applicationBusinessTime")
                or source.get("business_time")
                or source.get("businessTime")
            )
            if str(value or "").strip():
                return str(value).strip()
    if claim.occurred_at is not None:
        return claim.occurred_at.date().isoformat()
    return ""


def _resolve_required_application_status_label(status: Any) -> str:
    normalized = str(status or "").strip().lower()
    return {
        "approved": "已审批",
        "completed": "已完成",
    }.get(normalized, normalized)


def _attach_conversation_state(
    db: Session,
    payload: StewardPlanRequest,
    plan: StewardPlanResponse,
    *,
    current_user: CurrentUserContext,
) -> StewardPlanResponse:
    context_json = dict(payload.context_json or {})
    context_json["session_type"] = str(context_json.get("session_type") or "steward").strip() or "steward"
    conversation_service = AgentConversationService(db)
    conversation = conversation_service.get_or_create_conversation(
        conversation_id=_resolve_conversation_id(context_json),
        user_id=_require_steward_user_id(current_user),
        source="user_message",
        context_json=context_json,
    )
    _require_steward_conversation_access(conversation, current_user)
    current_state = _resolve_current_steward_state(conversation.state_json, context_json)
    steward_state = StewardFlowStateService().merge_plan(current_state, plan)
    conversation = conversation_service.update_state(
        conversation_id=conversation.conversation_id,
        run_id=None,
        scenario="steward",
        intent="plan",
        context_json={
            **context_json,
            "steward_state": steward_state,
        },
    ) or conversation
    conversation_service.append_message(
        conversation_id=conversation.conversation_id,
        role="user",
        content=payload.message,
        message_json={"source": "steward_plan_request"},
    )
    conversation_service.append_message(
        conversation_id=conversation.conversation_id,
        role="assistant",
        content=plan.summary,
        message_json={
            "source": "steward_plan_response",
            "plan_id": plan.plan_id,
            "steward_state": steward_state,
        },
    )
    return plan.model_copy(
        update={
            "conversation_id": conversation.conversation_id,
            "steward_state": steward_state,
        }
    )


def _attach_runtime_conversation_state(
    db: Session,
    payload: StewardRuntimeDecisionRequest,
    decision: StewardRuntimeDecisionResponse,
    *,
    current_user: CurrentUserContext,
) -> StewardRuntimeDecisionResponse:
    steward_state = decision.steward_state
    if not isinstance(steward_state, dict) or not steward_state:
        return decision
    context_json = dict(payload.context_json or {})
    conversation_id = _resolve_conversation_id(context_json)
    if not conversation_id:
        return decision

    conversation_service = AgentConversationService(db)
    conversation = conversation_service.get_conversation(conversation_id)
    if conversation is None:
        return decision
    _require_steward_conversation_access(conversation, current_user)
    conversation_service.update_state(
        conversation_id=conversation_id,
        run_id=None,
        scenario="steward",
        intent="runtime_decision",
        context_json={
            **context_json,
            "steward_state": steward_state,
        },
    )
    return decision


def _hydrate_runtime_decision_payload(
    db: Session,
    payload: StewardRuntimeDecisionRequest,
    *,
    current_user: CurrentUserContext,
) -> StewardRuntimeDecisionRequest:
    context_json = dict(payload.context_json or {})
    runtime_state = dict(payload.runtime_state or {})
    conversation_id = _resolve_conversation_id(context_json)
    conversation = (
        AgentConversationService(db).get_conversation(conversation_id)
        if conversation_id
        else None
    )
    if conversation is not None:
        _require_steward_conversation_access(conversation, current_user)
    if isinstance(runtime_state.get("steward_state"), dict) and runtime_state["steward_state"]:
        return payload
    if isinstance(context_json.get("steward_state"), dict) and context_json["steward_state"]:
        return payload

    if not conversation_id:
        return payload
    stored_state = conversation.state_json.get("steward_state") if conversation and isinstance(conversation.state_json, dict) else None
    if not isinstance(stored_state, dict) or not stored_state:
        return payload

    runtime_state["steward_state"] = stored_state
    conversation_state = dict(context_json.get("conversation_state") or {})
    conversation_state["steward_state"] = stored_state
    context_json["conversation_state"] = conversation_state
    return payload.model_copy(
        update={
            "runtime_state": runtime_state,
            "context_json": context_json,
        }
    )


def _require_steward_conversation_access(
    conversation: Any,
    current_user: CurrentUserContext,
) -> None:
    expected_tenant_id = _require_steward_tenant_id(current_user)
    expected_user_id = _require_steward_user_id(current_user)
    state_json = (
        dict(conversation.state_json)
        if isinstance(conversation.state_json, dict)
        else {}
    )
    conversation_tenant_id = str(state_json.get("tenant_id") or "").strip()
    conversation_user_id = str(conversation.user_id or "").strip()
    if (
        conversation_tenant_id == expected_tenant_id
        and conversation_user_id == expected_user_id
    ):
        return
    raise HTTPException(
        status_code=status.HTTP_403_FORBIDDEN,
        detail="当前会话不属于登录用户或租户，已拒绝访问。",
    )


def _resolve_conversation_id(context_json: dict[str, Any]) -> str | None:
    return str(
        context_json.get("conversation_id")
        or context_json.get("conversationId")
        or ""
    ).strip() or None


def _resolve_current_steward_state(
    conversation_state: dict[str, Any] | None,
    context_json: dict[str, Any],
) -> dict[str, Any]:
    state_json = conversation_state if isinstance(conversation_state, dict) else {}
    stored_state = state_json.get("steward_state")
    if isinstance(stored_state, dict) and stored_state:
        return stored_state
    incoming_state = context_json.get("steward_state") or context_json.get("stewardState")
    return incoming_state if isinstance(incoming_state, dict) else {}


def _inject_recent_conversation_history(
    db: Session,
    payload: StewardPlanRequest,
) -> StewardPlanRequest:
    """读取会话最近 10 条对话历史，注入 context_json.recent_history 供 LLM 关联上下文。

    历史只给模型用，不返回前端。在 get_or_create_conversation 之前读取，
    使用前端传入的 conversation_id，避免把本轮消息算进历史。
    """
    context_json = dict(payload.context_json or {})
    conversation_id = _resolve_conversation_id(context_json)
    if not conversation_id:
        return payload
    try:
        recent_history = AgentConversationService(db).list_message_history(
            conversation_id,
            limit=10,
        )
    except Exception:
        recent_history = []
    if not recent_history:
        return payload
    return payload.model_copy(
        update={
            "context_json": {
                **context_json,
                "recent_history": recent_history,
            }
        }
    )


def _apply_context_resume(
    db: Session,
    payload: StewardPlanRequest,
    plan: StewardPlanResponse,
) -> StewardPlanResponse:
    """确定性兜底：若 plan 无 task 且用户说"再提交"类话术，从会话状态恢复最近 task。

    不依赖 LLM 理解力，100% 可靠地恢复上下文。LLM 注入历史（保险②）覆盖更模糊话术。
    """
    if plan.tasks or plan.candidate_flows:
        return plan
    context_json = dict(payload.context_json or {})
    conversation_id = _resolve_conversation_id(context_json)
    if not conversation_id:
        return plan
    try:
        conversation = AgentConversationService(db).get_conversation(conversation_id)
    except Exception:
        conversation = None
    if conversation is None:
        return plan
    current_state = _resolve_current_steward_state(
        conversation.state_json if isinstance(conversation.state_json, dict) else {},
        context_json,
    )
    resume_flow_id = should_resume_recent_task(payload.message, current_state)
    if not resume_flow_id:
        return plan
    return attach_resumed_task(plan, current_state, resume_flow_id)
