from __future__ import annotations

from datetime import UTC, date, datetime, timedelta
from typing import Any

from sqlalchemy import or_, select
from sqlalchemy.orm import Session, selectinload

from app.core.agent_enums import AgentName, AgentRunSource
from app.db.schema_ownership import create_legacy_schema
from app.models.agent_run import AgentRun, AgentToolCall
from app.schemas.digital_employee_dashboard import DigitalEmployeeDashboardRead
from app.services.finance_report_tenant import require_report_tenant_id

SUCCESS_STATUSES = {"success", "succeeded", "ok", "done", "completed"}
FAILED_STATUSES = {"failed", "failure", "error", "errored"}
RUNNING_STATUSES = {"running", "pending"}

TASK_CODE_TO_TYPE = {
    "task.hermes.global_risk_scan": "global_risk_scan",
    "task.hermes.employee_behavior_profile_scan": "employee_behavior_profile_scan",
    "task.hermes.risk_rule_discovery": "risk_clue_collect",
    "task.hermes.finance_policy_knowledge_organize": "finance_policy_knowledge_organize",
    "task.hermes.finance_policy_clause_extract": "finance_policy_clause_extract",
    "task.hermes.expense_policy_alignment": "expense_policy_alignment",
    "task.hermes.risk_rule_template_organize": "risk_rule_template_organize",
    "task.hermes.department_expense_baseline_accumulate": "department_expense_baseline_accumulate",
    "task.hermes.supplier_risk_profile_accumulate": "supplier_risk_profile_accumulate",
    "task.hermes.false_positive_sample_accumulate": "false_positive_sample_accumulate",
    "task.hermes.risk_feedback_sample_accumulate": "risk_feedback_sample_accumulate",
    "task.hermes.multi_evidence_consistency_evaluate": "multi_evidence_consistency_evaluate",
    "task.hermes.travel_spatiotemporal_consistency_evaluate": (
        "travel_spatiotemporal_consistency_evaluate"
    ),
    "task.hermes.budget_overrun_precontrol_evaluate": "budget_overrun_precontrol_evaluate",
    "task.hermes.supplier_abnormal_relation_evaluate": "supplier_abnormal_relation_evaluate",
    "task.hermes.risk_algorithm_replay_evaluate": "risk_algorithm_replay_evaluate",
    "task.hermes.policy_gap_rule_optimization": "policy_gap_rule_optimization",
}

TASK_SPECS: dict[str, dict[str, str]] = {
    "global_risk_scan": {
        "label": "财务风险图谱巡检",
        "category": "评估",
        "color": "var(--theme-primary)",
    },
    "employee_behavior_profile_scan": {
        "label": "员工行为画像巡检",
        "category": "积累",
        "color": "var(--chart-blue)",
    },
    "risk_clue_collect": {
        "label": "风险线索归集",
        "category": "升级",
        "color": "var(--chart-amber)",
    },
    "finance_policy_knowledge_organize": {
        "label": "知识制度整理",
        "category": "整理",
        "color": "var(--success)",
    },
    "knowledge_index_sync": {
        "label": "知识制度整理",
        "category": "整理",
        "color": "var(--success)",
    },
    "llm_wiki_sync": {
        "label": "知识制度整理",
        "category": "整理",
        "color": "var(--success)",
    },
    "llm_wiki_rule_formation": {
        "label": "知识制度整理",
        "category": "整理",
        "color": "var(--success)",
    },
    "finance_policy_clause_extract": {
        "label": "制度条款结构化抽取",
        "category": "整理",
        "color": "var(--success)",
    },
    "expense_policy_alignment": {
        "label": "报销政策口径对齐",
        "category": "整理",
        "color": "var(--success)",
    },
    "risk_rule_template_organize": {
        "label": "规则命中样本整理",
        "category": "整理",
        "color": "var(--success)",
    },
    "department_expense_baseline_accumulate": {
        "label": "部门费用基线沉淀",
        "category": "积累",
        "color": "var(--chart-blue)",
    },
    "supplier_risk_profile_accumulate": {
        "label": "供应商风险画像沉淀",
        "category": "积累",
        "color": "var(--chart-blue)",
    },
    "false_positive_sample_accumulate": {
        "label": "历史误报样本沉淀",
        "category": "积累",
        "color": "var(--chart-blue)",
    },
    "risk_feedback_sample_accumulate": {
        "label": "风险观察反馈样本沉淀",
        "category": "积累",
        "color": "var(--chart-blue)",
    },
    "multi_evidence_consistency_evaluate": {
        "label": "多源证据一致性评估",
        "category": "评估",
        "color": "var(--theme-primary)",
    },
    "travel_spatiotemporal_consistency_evaluate": {
        "label": "差旅时空一致性评估",
        "category": "评估",
        "color": "var(--theme-primary)",
    },
    "budget_overrun_precontrol_evaluate": {
        "label": "预算超限预警评估",
        "category": "评估",
        "color": "var(--theme-primary)",
    },
    "supplier_abnormal_relation_evaluate": {
        "label": "供应商异常关系评估",
        "category": "评估",
        "color": "var(--theme-primary)",
    },
    "risk_algorithm_replay_evaluate": {
        "label": "风险算法回放升级",
        "category": "升级",
        "color": "var(--chart-amber)",
    },
    "policy_gap_rule_optimization": {
        "label": "制度缺口优化建议",
        "category": "升级",
        "color": "var(--chart-amber)",
    },
    "finance_dashboard_snapshot": {
        "label": "财务看板指标快照",
        "category": "积累",
        "color": "var(--chart-blue)",
    },
    "digital_employee_reminder_scan": {
        "label": "定时提醒扫描",
        "category": "整理",
        "color": "var(--success)",
    },
}

CATEGORY_SPECS = {
    "积累": {"color": "var(--chart-blue)", "description": "沉淀画像、基线和反馈样本"},
    "升级": {"color": "var(--chart-amber)", "description": "输出待复核线索和优化建议"},
    "整理": {"color": "var(--success)", "description": "整理制度、条款、知识和样本"},
    "评估": {"color": "var(--theme-primary)", "description": "评估异常、风险和一致性"},
}


class DigitalEmployeeDashboardService:
    def __init__(self, db: Session, *, tenant_id: str = "default") -> None:
        self.db = db
        self.tenant_id = require_report_tenant_id(tenant_id)

    def build_dashboard(
        self,
        *,
        days: int = 7,
        limit: int = 300,
        now: datetime | None = None,
    ) -> DigitalEmployeeDashboardRead:
        window_days = max(1, min(int(days or 7), 30))
        window_limit = max(1, min(int(limit or 300), 1000))
        self._ensure_storage_ready()
        generated_at = self._as_utc(now or datetime.now(UTC))
        start = generated_at - timedelta(days=window_days - 1)
        labels = self._date_labels(start.date(), window_days)

        all_runs = self._fetch_runs(start=start, limit=window_limit)
        runs = [run for run in all_runs if self._is_digital_employee_run(run)]
        totals = self._build_totals(runs)

        return DigitalEmployeeDashboardRead(
            window_days=window_days,
            generated_at=generated_at.isoformat(),
            has_real_data=bool(runs),
            totals=totals,
            daily_work=self._daily_work(labels, runs),
            task_distribution=self._task_distribution(runs),
            category_distribution=self._category_distribution(runs),
            recent_runs=self._recent_runs(runs),
        )

    def _ensure_storage_ready(self) -> None:
        create_legacy_schema(self.db.get_bind())

    def _fetch_runs(self, *, start: datetime, limit: int) -> list[AgentRun]:
        stmt = (
            select(AgentRun)
            .options(selectinload(AgentRun.tool_calls))
            .where(
                AgentRun.route_json["tenant_id"].as_string() == self.tenant_id,
                AgentRun.ontology_json["tenant_id"].as_string() == self.tenant_id,
                AgentRun.started_at >= start,
                or_(
                    AgentRun.agent == AgentName.HERMES.value,
                    AgentRun.source == AgentRunSource.SCHEDULE.value,
                ),
            )
            .order_by(AgentRun.started_at.desc())
            .limit(limit)
        )
        return list(self.db.scalars(stmt).all())

    def _build_totals(self, runs: list[AgentRun]) -> dict[str, Any]:
        metrics = self._sum_metrics(runs)
        success_runs = sum(1 for run in runs if self._is_success(run.status))
        failed_runs = sum(1 for run in runs if self._is_failed(run.status))
        running_runs = sum(1 for run in runs if self._is_running(run.status))
        total_runs = len(runs)
        business_outputs = (
            metrics["risk_observations"]
            + metrics["risk_clues"]
            + metrics["profile_snapshots"]
            + metrics["knowledge_documents"]
            + metrics["finance_snapshots"]
            + metrics["reminders"]
        )

        return {
            "totalRuns": total_runs,
            "successRuns": success_runs,
            "failedRuns": failed_runs,
            "runningRuns": running_runs,
            "toolCalls": sum(len(run.tool_calls) for run in runs),
            "businessOutputs": business_outputs,
            "riskObservations": metrics["risk_observations"],
            "riskClues": metrics["risk_clues"],
            "profileSnapshots": metrics["profile_snapshots"],
            "knowledgeDocuments": metrics["knowledge_documents"],
            "financeDashboardSnapshots": metrics["finance_snapshots"],
            "reminders": metrics["reminders"],
            "successRate": self._percent(success_runs, total_runs),
            "failureRate": self._percent(failed_runs, total_runs),
        }

    def _daily_work(self, labels: list[str], runs: list[AgentRun]) -> list[dict[str, Any]]:
        rows = {
            label: {
                "date": label,
                "total": 0,
                "success": 0,
                "failed": 0,
                "running": 0,
                "riskObservations": 0,
                "riskClues": 0,
                "profileSnapshots": 0,
                "knowledgeDocuments": 0,
                "financeDashboardSnapshots": 0,
                "reminders": 0,
                "businessOutputs": 0,
            }
            for label in labels
        }

        for run in runs:
            label = self._date_label(run.started_at)
            if label not in rows:
                continue
            row = rows[label]
            metrics = self._extract_run_metrics(run)
            row["total"] += 1
            if self._is_success(run.status):
                row["success"] += 1
            elif self._is_failed(run.status):
                row["failed"] += 1
            elif self._is_running(run.status):
                row["running"] += 1
            row["riskObservations"] += metrics["risk_observations"]
            row["riskClues"] += metrics["risk_clues"]
            row["profileSnapshots"] += metrics["profile_snapshots"]
            row["knowledgeDocuments"] += metrics["knowledge_documents"]
            row["financeDashboardSnapshots"] += metrics["finance_snapshots"]
            row["reminders"] += metrics["reminders"]
            row["businessOutputs"] += (
                metrics["risk_observations"]
                + metrics["risk_clues"]
                + metrics["profile_snapshots"]
                + metrics["knowledge_documents"]
                + metrics["finance_snapshots"]
                + metrics["reminders"]
            )

        return [rows[label] for label in labels]

    def _task_distribution(self, runs: list[AgentRun]) -> list[dict[str, Any]]:
        buckets: dict[str, dict[str, Any]] = {}
        for run in runs:
            task_type = self._resolve_task_type(run)
            spec = self._task_spec(task_type)
            bucket = buckets.setdefault(
                task_type or "unknown",
                {
                    "taskType": task_type or "unknown",
                    "name": spec["label"],
                    "category": spec["category"],
                    "count": 0,
                    "success": 0,
                    "failed": 0,
                    "value": 0,
                    "color": spec["color"],
                },
            )
            bucket["count"] += 1
            bucket["value"] += 1
            if self._is_success(run.status):
                bucket["success"] += 1
            elif self._is_failed(run.status):
                bucket["failed"] += 1

        return sorted(buckets.values(), key=lambda item: (-item["count"], item["name"]))[:8]

    def _category_distribution(self, runs: list[AgentRun]) -> list[dict[str, Any]]:
        rows = {
            category: {
                "name": category,
                "value": 0,
                "count": 0,
                "success": 0,
                "failed": 0,
                "color": spec["color"],
                "description": spec["description"],
            }
            for category, spec in CATEGORY_SPECS.items()
        }
        for run in runs:
            category = self._task_spec(self._resolve_task_type(run))["category"]
            row = rows.setdefault(
                category,
                {
                    "name": category,
                    "value": 0,
                    "count": 0,
                    "success": 0,
                    "failed": 0,
                    "color": "var(--theme-primary)",
                    "description": "其他数字员工工作",
                },
            )
            row["value"] += 1
            row["count"] += 1
            if self._is_success(run.status):
                row["success"] += 1
            elif self._is_failed(run.status):
                row["failed"] += 1
        return list(rows.values())

    def _recent_runs(self, runs: list[AgentRun]) -> list[dict[str, Any]]:
        rows = []
        for run in sorted(runs, key=lambda item: item.started_at, reverse=True)[:8]:
            task_type = self._resolve_task_type(run)
            spec = self._task_spec(task_type)
            rows.append(
                {
                    "runId": run.run_id,
                    "taskType": task_type or "unknown",
                    "taskLabel": spec["label"],
                    "category": spec["category"],
                    "status": run.status,
                    "statusLabel": self._status_label(run.status),
                    "statusTone": self._status_tone(run.status),
                    "source": run.source,
                    "sourceLabel": self._source_label(run.source),
                    "startedAt": self._iso(run.started_at),
                    "finishedAt": self._iso(run.finished_at),
                    "durationMs": self._duration_ms(run),
                    "summary": self._summary_text(run),
                    "metrics": self._extract_run_metrics(run),
                }
            )
        return rows

    def _sum_metrics(self, runs: list[AgentRun]) -> dict[str, int]:
        totals = self._empty_metrics()
        for run in runs:
            metrics = self._extract_run_metrics(run)
            for key in totals:
                totals[key] += int(metrics.get(key) or 0)
        return totals

    def _extract_run_metrics(self, run: AgentRun) -> dict[str, int]:
        summary = self._extract_run_summary(run)
        route_json = run.route_json or {}
        metrics = self._empty_metrics()
        metrics["risk_observations"] = self._first_int(
            summary,
            ("risk_observation_count", "risk_observations", "created_observation_count"),
        )
        metrics["risk_clues"] = self._first_int(
            summary,
            ("risk_clue_count", "risk_clues", "created_clue_count"),
        )
        metrics["profile_snapshots"] = self._first_int(
            summary,
            ("snapshot_count", "profile_snapshot_count", "profile_snapshots"),
        )
        if self._resolve_task_type(run) == "finance_dashboard_snapshot":
            metrics["profile_snapshots"] = 0
            metrics["finance_snapshots"] = self._first_int(
                summary,
                ("finance_snapshot_count", "dashboard_snapshot_count"),
            )
        metrics["knowledge_documents"] = max(
            self._first_int(
                summary,
                ("knowledge_document_count", "document_count", "processed_document_count"),
            ),
            self._list_length(summary, ("document_ids", "requested_document_ids")),
            self._list_length(route_json, ("document_ids", "requested_document_ids")),
        )
        metrics["scanned_claims"] = self._first_int(summary, ("scanned_claim_count", "claim_count"))
        metrics["target_employees"] = self._first_int(
            summary,
            ("target_employee_count", "employee_count"),
        )
        metrics["rule_hits"] = self._first_int(summary, ("rule_hit_count", "rule_hits"))
        metrics["facts"] = self._first_int(summary, ("fact_count", "facts"))
        metrics["reminders"] = self._first_int(
            summary,
            (
                "reminder_count",
                "reminders",
                "approval_pending_count",
                "budget_reminder_count",
            ),
        )
        return metrics

    @staticmethod
    def _empty_metrics() -> dict[str, int]:
        return {
            "risk_observations": 0,
            "risk_clues": 0,
            "profile_snapshots": 0,
            "finance_snapshots": 0,
            "knowledge_documents": 0,
            "scanned_claims": 0,
            "target_employees": 0,
            "rule_hits": 0,
            "facts": 0,
            "reminders": 0,
        }

    def _extract_run_summary(self, run: AgentRun) -> dict[str, Any]:
        task_type = self._resolve_task_type(run)
        matched_tool = self._matched_tool_call(run, task_type)
        if matched_tool is None:
            return run.route_json or {}
        response = matched_tool.response_json or {}
        if isinstance(response, dict) and isinstance(response.get("summary"), dict):
            return response["summary"]
        return response if isinstance(response, dict) else {}

    def _matched_tool_call(self, run: AgentRun, task_type: str) -> AgentToolCall | None:
        digital_tools = [
            tool
            for tool in run.tool_calls
            if str(tool.tool_name or "").startswith("digital_employee.")
        ]
        for tool in run.tool_calls:
            candidates = [
                (tool.request_json or {}).get("task_type"),
                (tool.request_json or {}).get("job_type"),
                (tool.response_json or {}).get("report_type"),
                (tool.response_json or {}).get("task_type"),
                (tool.response_json or {}).get("job_type"),
                self._task_type_from_tool_name(tool.tool_name),
            ]
            if task_type and task_type in {self._normalize_task_type(item) for item in candidates}:
                return tool
        if digital_tools:
            return digital_tools[0]
        return run.tool_calls[0] if run.tool_calls else None

    def _is_digital_employee_run(self, run: AgentRun) -> bool:
        task_type = self._resolve_task_type(run)
        if task_type in TASK_SPECS:
            return True
        if run.agent == AgentName.HERMES.value:
            return True
        if run.source == AgentRunSource.SCHEDULE.value and task_type:
            return True
        route_json = run.route_json or {}
        if str(route_json.get("selected_agent") or "").strip() == AgentName.HERMES.value:
            return True
        return any(
            str(tool.tool_name or "").startswith("digital_employee.") for tool in run.tool_calls
        )

    def _resolve_task_type(self, run: AgentRun) -> str:
        route_json = run.route_json or {}
        route_candidates = [
            route_json.get("job_type"),
            route_json.get("task_type"),
            route_json.get("report_type"),
            route_json.get("task_code"),
            route_json.get("code"),
        ]
        for candidate in route_candidates:
            normalized = self._normalize_task_type(candidate)
            if normalized:
                return normalized

        for tool in run.tool_calls:
            for candidate in (
                (tool.request_json or {}).get("task_type"),
                (tool.request_json or {}).get("job_type"),
                (tool.response_json or {}).get("report_type"),
                (tool.response_json or {}).get("task_type"),
                (tool.response_json or {}).get("job_type"),
                self._task_type_from_tool_name(tool.tool_name),
            ):
                normalized = self._normalize_task_type(candidate)
                if normalized:
                    return normalized
        return ""

    @staticmethod
    def _normalize_task_type(value: Any) -> str:
        text = str(value or "").strip()
        if not text:
            return ""
        text = TASK_CODE_TO_TYPE.get(text, text)
        if text.startswith("task.hermes."):
            text = text.removeprefix("task.hermes.")
        text = text.replace("-", "_").replace(".", "_")
        if text == "risk_rule_discovery":
            return "risk_clue_collect"
        return text

    @staticmethod
    def _task_type_from_tool_name(value: str | None) -> str:
        name = str(value or "")
        if "financial_risk_graph" in name:
            return "global_risk_scan"
        if "employee_behavior_profile" in name:
            return "employee_behavior_profile_scan"
        if "reminder" in name:
            return "digital_employee_reminder_scan"
        if "finance_policy_knowledge" in name:
            return "finance_policy_knowledge_organize"
        if "risk_clue" in name:
            return "risk_clue_collect"
        return ""

    @staticmethod
    def _task_spec(task_type: str) -> dict[str, str]:
        return TASK_SPECS.get(
            task_type,
            {
                "label": "数字员工工作",
                "category": "评估",
                "color": "var(--theme-primary)",
            },
        )

    def _summary_text(self, run: AgentRun) -> str:
        text = str(run.result_summary or "").strip()
        if text:
            return text
        summary = self._extract_run_summary(run)
        for key in ("message", "summary", "result_summary"):
            value = str(summary.get(key) or "").strip()
            if value:
                return value
        if run.error_message:
            return str(run.error_message)
        return "暂无摘要。"

    @staticmethod
    def _first_int(payload: Any, keys: tuple[str, ...]) -> int:
        if isinstance(payload, dict):
            for key in keys:
                value = payload.get(key)
                if isinstance(value, (int, float)) and value > 0:
                    return int(value)
            for value in payload.values():
                found = DigitalEmployeeDashboardService._first_int(value, keys)
                if found:
                    return found
        if isinstance(payload, list):
            for value in payload:
                found = DigitalEmployeeDashboardService._first_int(value, keys)
                if found:
                    return found
        return 0

    @staticmethod
    def _list_length(payload: Any, keys: tuple[str, ...]) -> int:
        if isinstance(payload, dict):
            for key in keys:
                value = payload.get(key)
                if isinstance(value, list):
                    return len(value)
            for value in payload.values():
                found = DigitalEmployeeDashboardService._list_length(value, keys)
                if found:
                    return found
        if isinstance(payload, list):
            for value in payload:
                found = DigitalEmployeeDashboardService._list_length(value, keys)
                if found:
                    return found
        return 0

    @staticmethod
    def _percent(value: int | float, total: int | float) -> float:
        if not total:
            return 0.0
        return round((float(value) / float(total)) * 100, 1)

    @staticmethod
    def _duration_ms(run: AgentRun) -> int:
        if not run.finished_at:
            return 0
        try:
            finished_at = DigitalEmployeeDashboardService._as_utc(run.finished_at)
            started_at = DigitalEmployeeDashboardService._as_utc(run.started_at)
            return max(0, int((finished_at - started_at).total_seconds() * 1000))
        except TypeError:
            return 0

    @staticmethod
    def _date_labels(start_date: date, days: int) -> list[str]:
        return [(start_date + timedelta(days=index)).strftime("%m-%d") for index in range(days)]

    @staticmethod
    def _date_label(value: datetime | None) -> str:
        if value is None:
            return ""
        return DigitalEmployeeDashboardService._as_utc(value).strftime("%m-%d")

    @staticmethod
    def _iso(value: datetime | None) -> str:
        if value is None:
            return ""
        return DigitalEmployeeDashboardService._as_utc(value).isoformat()

    @staticmethod
    def _as_utc(value: datetime) -> datetime:
        if value.tzinfo is None:
            return value.replace(tzinfo=UTC)
        return value.astimezone(UTC)

    @staticmethod
    def _is_success(status: str | None) -> bool:
        return str(status or "").strip().lower() in SUCCESS_STATUSES

    @staticmethod
    def _is_failed(status: str | None) -> bool:
        return str(status or "").strip().lower() in FAILED_STATUSES

    @staticmethod
    def _is_running(status: str | None) -> bool:
        return str(status or "").strip().lower() in RUNNING_STATUSES

    def _status_label(self, status: str | None) -> str:
        if self._is_success(status):
            return "成功"
        if self._is_failed(status):
            return "失败"
        if self._is_running(status):
            return "运行中"
        return str(status or "其他")

    def _status_tone(self, status: str | None) -> str:
        if self._is_success(status):
            return "success"
        if self._is_failed(status):
            return "danger"
        if self._is_running(status):
            return "warning"
        return "neutral"

    @staticmethod
    def _source_label(source: str | None) -> str:
        labels = {
            "schedule": "定时任务",
            "system_event": "系统事件",
            "user_message": "用户触发",
        }
        text = str(source or "").strip()
        return labels.get(text, text or "未标记")
