from __future__ import annotations

import re
from datetime import UTC, datetime
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.agent_enums import (
    AgentName,
    AgentPermissionLevel,
    AgentRunSource,
    AgentRunStatus,
    AgentToolType,
)
from app.core.logging import get_logger
from app.models.employee import Employee
from app.models.financial_record import (
    AccountsPayableRecord,
    AccountsReceivableRecord,
    ExpenseClaim,
)
from app.models.organization import OrganizationUnit
from app.schemas.ontology import (
    OntologyParseRequest,
    OntologyParseResult,
)
from app.services.agent_foundation import AgentFoundationService
from app.services.agent_runs import AgentRunService
from app.services.ontology_detection import OntologyDetectionMixin
from app.services.ontology_extraction import OntologyExtractionMixin
from app.services.ontology_field_registry import normalize_ontology_context_json
from app.services.ontology_rules import (
    CONTEXTUAL_SCENARIOS,
    EXPENSE_REVIEW_ACTIONS,
    LlmOntologyParseResult,
    ReferenceCatalog,
)
from app.services.ontology_validation import OntologyValidationMixin
from app.services.runtime_chat import RuntimeChatService
from app.services.runtime_chat_attempts import RuntimeChatOperationContext
from app.services.runtime_chat_commercial import (
    build_runtime_chat_commercial_observer,
    trusted_runtime_chat_operation_context,
)

logger = get_logger("app.services.ontology")


class SemanticOntologyService(
    OntologyDetectionMixin,
    OntologyExtractionMixin,
    OntologyValidationMixin,
):
    def __init__(self, db: Session) -> None:
        self.db = db
        self.run_service = AgentRunService(db)
        self.runtime_chat_service = RuntimeChatService(
            db,
            attempt_observer=build_runtime_chat_commercial_observer(db),
        )

    def parse(
        self,
        payload: OntologyParseRequest,
        *,
        tenant_id: str = "default",
    ) -> OntologyParseResult:
        tenant = self._require_tenant_id(tenant_id)
        run = self.run_service.create_run(
            agent=AgentName.ORCHESTRATOR.value,
            source=AgentRunSource.USER_MESSAGE.value,
            tenant_id=tenant,
            user_id=payload.user_id,
            ontology_json={"scenario": "unknown", "intent": "query"},
            route_json={
                "stage": "semantic_parse",
                "phase": "pending_model_analysis",
            },
            permission_level=AgentPermissionLevel.READ.value,
            status=AgentRunStatus.RUNNING.value,
            result_summary="语义解析请求已接收。",
        )
        operation_context = trusted_runtime_chat_operation_context(
            self.db,
            run_id=run.run_id,
            attempt_scope="semantic-ontology-parse",
        )
        if operation_context is None:
            self._mark_failed(run.run_id, "可信租户运行上下文缺失。")
            raise ValueError("可信租户运行上下文缺失。")
        try:
            analyzed = self._analyze(
                payload,
                tenant_id=tenant,
                operation_context=operation_context,
            )
            self._record_semantic_parse(
                run_id=run.run_id,
                payload=payload,
                analyzed=analyzed,
            )
            self._record_model_invocations(run_id=run.run_id, analyzed=analyzed)
            final_status = (
                AgentRunStatus.BLOCKED.value
                if analyzed["clarification_required"]
                or analyzed["permission"].level == AgentPermissionLevel.FORBIDDEN.value
                else AgentRunStatus.SUCCEEDED.value
            )
            self.run_service.update_run(
                run.run_id,
                ontology_json=self._build_ontology_json(analyzed),
                route_json={
                    "stage": "semantic_parse",
                    "phase": "completed",
                    "model_invocation_summary": self._build_model_invocation_summary(analyzed),
                    "clarification_required": analyzed["clarification_required"],
                    "field_error_count": len(analyzed["field_errors"]),
                },
                permission_level=analyzed["permission"].level,
                status=final_status,
                result_summary=self._build_result_summary(
                    analyzed["scenario"],
                    analyzed["intent"],
                    analyzed["permission"].level,
                    analyzed["confidence"],
                ),
                error_message=(
                    analyzed["permission"].reason
                    if analyzed["permission"].level == AgentPermissionLevel.FORBIDDEN.value
                    else None
                ),
                finished_at=datetime.now(UTC),
            )
            return self._build_result(analyzed, run.run_id)
        except Exception as exc:
            self._mark_failed(run.run_id, str(exc))
            raise

    def parse_for_run(self, payload: OntologyParseRequest, *, run_id: str) -> OntologyParseResult:
        operation_context = trusted_runtime_chat_operation_context(
            self.db,
            run_id=run_id,
            attempt_scope="orchestrator-semantic-ontology",
        )
        if operation_context is None:
            raise ValueError("语义解析运行缺少可信租户上下文。")
        analyzed = self._analyze(
            payload,
            tenant_id=operation_context.tenant_id,
            operation_context=operation_context,
        )
        self._record_semantic_parse(run_id=run_id, payload=payload, analyzed=analyzed)
        self._record_model_invocations(run_id=run_id, analyzed=analyzed)
        return self._build_result(analyzed, run_id)

    def _analyze(
        self,
        payload: OntologyParseRequest,
        *,
        tenant_id: str,
        operation_context: RuntimeChatOperationContext | None,
    ) -> dict[str, object]:
        query = payload.query.strip()
        if not query:
            raise ValueError("query 不能为空。")

        context_json = normalize_ontology_context_json(payload.context_json or {})
        payload = payload.model_copy(update={"context_json": context_json})
        compact_query = self._compact(query)
        if not self._has_supported_business_signal(compact_query, context_json):
            raise ValueError("当前系统仅支持财务业务相关问题。")

        AgentFoundationService(self.db).ensure_foundation_ready()
        reference = self._load_reference_catalog(tenant_id=tenant_id)
        entities = self._extract_entities(
            query,
            compact_query,
            reference,
            context_json=context_json,
        )
        rule_scenario, scenario_score = self._detect_scenario(compact_query)
        time_range, _time_score = self._extract_time_range(
            query,
            compact_query,
            context_json=context_json,
        )
        session_scenario = self._resolve_session_type_scenario(context_json)
        context_scenario = self._resolve_context_scenario(context_json)
        application_context = self._is_expense_application_context(context_json)
        application_query = self._looks_like_expense_application(compact_query)
        if session_scenario == "knowledge":
            rule_scenario = "knowledge"
            scenario_score = max(scenario_score, 0.34)
        if session_scenario != "knowledge" and (application_context or application_query):
            rule_scenario = "expense"
            scenario_score = max(scenario_score, 0.22)
        if rule_scenario == "unknown" and context_scenario is not None:
            rule_scenario = context_scenario
            scenario_score = max(scenario_score, 0.14)
        if rule_scenario == "unknown":
            inferred_scenario = self._infer_scenario_from_entities(entities)
            if inferred_scenario is not None:
                rule_scenario = inferred_scenario
                scenario_score = 0.18

        if session_scenario != "knowledge" and self._looks_like_expense_narrative(
            compact_query,
            scenario=rule_scenario,
            entities=entities,
            time_range=time_range,
        ):
            rule_scenario = "expense"
            scenario_score = max(scenario_score, 0.24)

        rule_intent, intent_score = self._detect_intent(
            compact_query,
            scenario=rule_scenario,
            entities=entities,
            time_range=time_range,
        )
        if session_scenario != "knowledge" and (application_context or application_query):
            rule_intent = "draft"
            intent_score = max(intent_score, 0.22)
        if session_scenario != "knowledge" and self._should_inherit_expense_draft(
            compact_query,
            scenario=rule_scenario,
            entities=entities,
            time_range=time_range,
            context_json=context_json,
        ):
            rule_scenario = "expense"
            rule_intent = "draft"
            scenario_score = max(scenario_score, 0.18)
            intent_score = max(intent_score, 0.18)
        metrics = self._extract_metrics(compact_query)
        constraints = self._extract_constraints(compact_query, entities)
        model_parse = None
        model_invocations: list[dict[str, Any]] = []
        model_parse_error = None
        if session_scenario != "knowledge":
            model_parse, model_invocations, model_parse_error = self._parse_with_model(
                payload=payload,
                query=query,
                compact_query=compact_query,
                fallback_scenario=rule_scenario,
                fallback_intent=rule_intent,
                entities=entities,
                time_range=time_range,
                metrics=metrics,
                constraints=constraints,
                operation_context=operation_context,
            )
        model_guardrail_reason = (
            self._resolve_model_guardrail_reason(
                model_parse,
                rule_scenario=rule_scenario,
                application_query=application_query,
            )
            if session_scenario != "knowledge"
            else None
        )
        accepted_model_parse = None if model_guardrail_reason else model_parse

        scenario = self._resolve_scenario(rule_scenario, accepted_model_parse)
        if session_scenario == "knowledge":
            scenario = "knowledge"
        entities = self._merge_entities(
            entities,
            accepted_model_parse.entity_hints if accepted_model_parse is not None else [],
            compact_query,
        )
        intent = self._resolve_intent(
            compact_query,
            fallback_intent=rule_intent,
            scenario=scenario,
            entities=entities,
            time_range=time_range,
            model_parse=accepted_model_parse,
        )
        missing_slots = self._normalize_short_text_list(
            accepted_model_parse.missing_slots if accepted_model_parse is not None else []
        )
        missing_slots = self._normalize_short_text_list(
            missing_slots
            + self._infer_default_missing_slots(
                compact_query,
                scenario=scenario,
                intent=intent,
                entities=entities,
                time_range=time_range,
                context_json=context_json,
            )
        )
        missing_slots = self._filter_expense_missing_slots(
            compact_query=compact_query,
            entities=entities,
            missing_slots=missing_slots,
        )
        relax_knowledge_follow_up = self._should_relax_knowledge_follow_up_clarification(
            compact_query=compact_query,
            scenario=scenario,
            context_json=context_json,
            missing_slots=missing_slots,
        )
        if relax_knowledge_follow_up:
            missing_slots = [item for item in missing_slots if item != "expense_type"]
        ambiguity = self._normalize_short_text_list(
            accepted_model_parse.ambiguity if accepted_model_parse is not None else []
        )
        risk_flags = self._extract_risk_flags(compact_query, scenario)
        permission = self._resolve_permission(
            compact_query,
            context_json,
            intent,
        )

        field_errors = self._build_field_errors(
            scenario=scenario,
            intent=intent,
            entities=entities,
            permission=permission,
            missing_slots=missing_slots,
            ambiguity=ambiguity,
        )
        clarification_required, clarification_question = self._build_clarification(
            scenario=scenario,
            intent=intent,
            entities=entities,
            permission=permission,
            missing_slots=missing_slots,
            ambiguity=ambiguity,
            allow_incomplete_draft=self._allow_incomplete_draft(
                context_json,
                scenario=scenario,
                intent=intent,
            ),
            model_clarification_required=bool(
                accepted_model_parse is not None and accepted_model_parse.clarification_required
            ),
            model_clarification_question=(
                accepted_model_parse.clarification_question
                if accepted_model_parse is not None
                else None
            ),
        )
        if relax_knowledge_follow_up:
            clarification_required = False
            clarification_question = None
        fallback_confidence = self._compute_confidence(
            scenario=scenario,
            scenario_score=scenario_score,
            intent_score=intent_score,
            entities=entities,
            time_range=time_range,
            metrics=metrics,
            constraints=constraints,
            risk_flags=risk_flags,
            clarification_required=clarification_required,
            permission=permission,
        )
        confidence = self._resolve_confidence(
            model_confidence=(
                accepted_model_parse.confidence if accepted_model_parse is not None else None
            ),
            fallback_confidence=fallback_confidence,
            clarification_required=clarification_required,
            permission=permission,
        )
        return {
            "scenario": scenario,
            "intent": intent,
            "entities": entities,
            "time_range": time_range,
            "metrics": metrics,
            "constraints": constraints,
            "risk_flags": risk_flags,
            "permission": permission,
            "confidence": confidence,
            "missing_slots": missing_slots,
            "ambiguity": ambiguity,
            "parse_strategy": (
                "llm_primary" if accepted_model_parse is not None else "rule_fallback"
            ),
            "model_invocations": model_invocations,
            "model_parse_error": model_parse_error,
            "model_guardrail_reason": model_guardrail_reason,
            "clarification_required": clarification_required,
            "clarification_question": clarification_question,
            "field_errors": field_errors,
        }

    @staticmethod
    def _resolve_model_guardrail_reason(
        model_parse: LlmOntologyParseResult | None,
        *,
        rule_scenario: str,
        application_query: bool,
    ) -> str | None:
        if model_parse is None:
            return "model_unavailable_or_invalid"
        if model_parse.confidence < 0.55:
            return "model_confidence_low"
        if model_parse.scenario == "unknown":
            return "model_scenario_unknown"
        if application_query and rule_scenario == "expense" and model_parse.scenario != "expense":
            return "model_conflicts_with_application_stage_signal"
        return None

    @staticmethod
    def _should_relax_knowledge_follow_up_clarification(
        *,
        compact_query: str,
        scenario: str,
        context_json: dict[str, Any],
        missing_slots: list[str],
    ) -> bool:
        if scenario != "knowledge" or "expense_type" not in missing_slots:
            return False
        history = context_json.get("conversation_history")
        if not isinstance(history, list):
            return False
        has_previous_user_turn = any(
            isinstance(item, dict)
            and str(item.get("role") or "").strip() == "user"
            and str(item.get("content") or "").strip()
            for item in history
        )
        if not has_previous_user_turn:
            return False
        follow_up_markers = ("那", "那么", "这个", "这种", "呢", "的话", "p", "P")
        return any(marker in compact_query for marker in follow_up_markers)

    @staticmethod
    def _filter_expense_missing_slots(
        *,
        compact_query: str,
        entities: list[object],
        missing_slots: list[str],
    ) -> list[str]:
        expense_types = {
            str(getattr(item, "normalized_value", "") or getattr(item, "value", "") or "").strip()
            for item in entities
            if getattr(item, "type", "") == "expense_type"
        }
        has_transport = "transport" in expense_types
        has_entertainment = "entertainment" in expense_types
        explicit_entertainment = any(
            keyword in compact_query
            for keyword in (
                "业务招待",
                "招待费",
                "招待",
                "宴请",
                "请客",
                "请客户吃饭",
                "客户吃饭",
                "客户用餐",
                "客户餐",
                "商务接待",
                "商务宴请",
                "接待餐",
            )
        )
        if has_transport and not has_entertainment and not explicit_entertainment:
            return [item for item in missing_slots if item not in {"customer_name", "participants"}]
        return missing_slots

    def _record_semantic_parse(
        self,
        *,
        run_id: str,
        payload: OntologyParseRequest,
        analyzed: dict[str, object],
    ) -> None:
        self.run_service.record_semantic_parse(
            run_id=run_id,
            user_id=payload.user_id,
            raw_query=payload.query.strip(),
            scenario=str(analyzed["scenario"]),
            intent=str(analyzed["intent"]),
            entities_json=[item.model_dump() for item in analyzed["entities"]],
            time_range_json=analyzed["time_range"].model_dump(),
            metrics_json=[item.model_dump() for item in analyzed["metrics"]],
            constraints_json=[item.model_dump() for item in analyzed["constraints"]],
            risk_flags_json=list(analyzed["risk_flags"]),
            permission_json=analyzed["permission"].model_dump(),
            confidence=float(analyzed["confidence"]),
        )
        logger.info(
            "Parsed ontology run_id=%s scenario=%s intent=%s permission=%s",
            run_id,
            analyzed["scenario"],
            analyzed["intent"],
            analyzed["permission"].level,
        )

    def _record_model_invocations(
        self,
        *,
        run_id: str,
        analyzed: dict[str, object],
    ) -> None:
        invocations = [
            item for item in list(analyzed.get("model_invocations") or []) if isinstance(item, dict)
        ]
        if not invocations:
            return

        parse_strategy = str(analyzed.get("parse_strategy") or "")
        parse_error = str(analyzed.get("model_parse_error") or "").strip()
        guardrail_reason = str(analyzed.get("model_guardrail_reason") or "").strip()
        for item in invocations:
            call_status = str(item.get("status") or "unknown").strip()
            slot = str(item.get("slot") or "unknown").strip()
            provider = str(item.get("provider") or "").strip()
            model = str(item.get("model") or "").strip()
            postprocess_error = parse_error or guardrail_reason
            status = "succeeded"
            error_message = str(item.get("error_message") or "").strip() or None
            if call_status == "skipped":
                status = "skipped"
                error_message = str(item.get("skipped_reason") or "").strip() or None
            elif call_status != "succeeded" or postprocess_error:
                status = "failed"
                error_message = error_message or postprocess_error or call_status

            self.run_service.record_tool_call(
                run_id=run_id,
                tool_type=AgentToolType.LLM.value,
                tool_name=f"semantic_ontology.{slot}",
                request_json={
                    "stage": "semantic_parse",
                    "slot": slot,
                    "provider": provider,
                    "model": model,
                    "attempt": item.get("attempt"),
                },
                response_json={
                    "model_call_status": call_status,
                    "parse_strategy": parse_strategy,
                    "model_parse_error": parse_error,
                    "model_guardrail_reason": guardrail_reason,
                    "duration_ms": item.get("duration_ms", 0),
                },
                status=status,
                duration_ms=int(item.get("duration_ms") or 0),
                error_message=error_message,
            )

    @staticmethod
    def _build_model_invocation_summary(analyzed: dict[str, object]) -> dict[str, object]:
        invocations = [
            item for item in list(analyzed.get("model_invocations") or []) if isinstance(item, dict)
        ]
        statuses = [str(item.get("status") or "unknown") for item in invocations]
        return {
            "attempt_count": len(invocations),
            "succeeded_count": statuses.count("succeeded"),
            "failed_count": statuses.count("failed") + statuses.count("empty"),
            "skipped_count": statuses.count("skipped"),
            "parse_strategy": analyzed.get("parse_strategy"),
            "model_parse_error": analyzed.get("model_parse_error"),
            "model_guardrail_reason": analyzed.get("model_guardrail_reason"),
        }

    @staticmethod
    def _build_ontology_json(analyzed: dict[str, object]) -> dict[str, object]:
        return {
            "scenario": analyzed["scenario"],
            "intent": analyzed["intent"],
            "entities": [item.model_dump() for item in analyzed["entities"]],
            "time_range": analyzed["time_range"].model_dump(),
            "metrics": [item.model_dump() for item in analyzed["metrics"]],
            "constraints": [item.model_dump() for item in analyzed["constraints"]],
            "risk_flags": list(analyzed["risk_flags"]),
            "permission": analyzed["permission"].model_dump(),
            "missing_slots": list(analyzed["missing_slots"]),
            "ambiguity": list(analyzed["ambiguity"]),
            "parse_strategy": analyzed["parse_strategy"],
            "model_invocation_summary": SemanticOntologyService._build_model_invocation_summary(
                analyzed
            ),
            "confidence": analyzed["confidence"],
        }

    @staticmethod
    def _build_result(analyzed: dict[str, object], run_id: str) -> OntologyParseResult:
        return OntologyParseResult(
            scenario=analyzed["scenario"],
            intent=analyzed["intent"],
            entities=analyzed["entities"],
            time_range=analyzed["time_range"],
            metrics=analyzed["metrics"],
            constraints=analyzed["constraints"],
            risk_flags=analyzed["risk_flags"],
            permission=analyzed["permission"],
            confidence=analyzed["confidence"],
            missing_slots=analyzed["missing_slots"],
            ambiguity=analyzed["ambiguity"],
            parse_strategy=analyzed["parse_strategy"],
            clarification_required=analyzed["clarification_required"],
            clarification_question=analyzed["clarification_question"],
            run_id=run_id,
            field_errors=analyzed["field_errors"],
        )

    def _load_reference_catalog(self, *, tenant_id: str) -> ReferenceCatalog:
        tenant = self._require_tenant_id(tenant_id)
        employees = self._read_distinct_values(
            select(Employee.name).where(Employee.tenant_id == tenant)
        )
        departments = self._read_distinct_values(
            select(OrganizationUnit.name).where(OrganizationUnit.tenant_id == tenant)
        )
        departments += self._read_distinct_values(
            select(ExpenseClaim.department_name).where(ExpenseClaim.tenant_id == tenant)
        )
        customers = self._read_distinct_values(
            select(AccountsReceivableRecord.customer_name).where(
                AccountsReceivableRecord.tenant_id == tenant
            )
        )
        vendors = self._read_distinct_values(
            select(AccountsPayableRecord.vendor_name).where(
                AccountsPayableRecord.tenant_id == tenant
            )
        )
        projects = self._read_distinct_values(
            select(ExpenseClaim.project_code).where(ExpenseClaim.tenant_id == tenant)
        )

        return ReferenceCatalog(
            employees=self._dedupe_and_sort(employees),
            departments=self._dedupe_and_sort(departments),
            customers=self._dedupe_and_sort(customers),
            vendors=self._dedupe_and_sort(vendors),
            projects=self._dedupe_and_sort(projects),
        )

    def _read_distinct_values(self, stmt) -> list[str]:
        values = self.db.scalars(stmt.distinct()).all()
        return [str(item).strip() for item in values if item]

    def _mark_failed(self, run_id: str, message: str) -> None:
        try:
            self.run_service.merge_route_json(
                run_id,
                {
                    "stage": "semantic_parse",
                    "phase": "failed",
                    "heartbeat_at": datetime.now(UTC).isoformat(),
                },
                status=AgentRunStatus.FAILED.value,
                error_message=str(message or "语义解析失败。")[:2000],
                finished_at=datetime.now(UTC),
            )
        except Exception:
            logger.exception("Failed to persist ontology run failure run_id=%s", run_id)

    @staticmethod
    def _require_tenant_id(value: object) -> str:
        tenant_id = str(value or "").strip()
        if not tenant_id:
            raise ValueError("tenant_id 不能为空。")
        return tenant_id

    @staticmethod
    def _dedupe_and_sort(values: list[str]) -> list[str]:
        items = {str(item).strip() for item in values if str(item).strip()}
        return sorted(items, key=lambda item: (-len(item), item))

    @staticmethod
    def _compact(text: str) -> str:
        return re.sub(r"\s+", "", text).lower()

    @staticmethod
    def _resolve_context_scenario(context_json: dict[str, Any]) -> str | None:
        value = str(context_json.get("conversation_scenario") or "").strip()
        if value in CONTEXTUAL_SCENARIOS:
            return value
        review_action = str(context_json.get("review_action") or "").strip()
        if review_action in EXPENSE_REVIEW_ACTIONS:
            return "expense"
        if str(context_json.get("draft_claim_id") or "").strip():
            return "expense"
        return None

    @staticmethod
    def _resolve_session_type_scenario(context_json: dict[str, Any]) -> str | None:
        value = str(context_json.get("session_type") or "").strip()
        if value == "knowledge":
            return "knowledge"
        return None
