from __future__ import annotations

import json
from dataclasses import dataclass, field
from datetime import UTC, date, datetime, timedelta
from typing import Any

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

from app.db.schema_ownership import create_legacy_schema
from app.models.agent_feedback import AgentOperationFeedback
from app.models.agent_run import AgentRun, AgentToolCall
from app.models.user_session_metric import UserSessionMetric
from app.schemas.system_dashboard import SystemDashboardRead

SUCCESS_STATUSES = {"success", "succeeded", "ok", "done", "completed"}
FAILED_STATUSES = {"failed", "failure", "error", "errored"}
BLOCKED_STATUSES = {"blocked", "forbidden", "rejected"}
RUNNING_STATUSES = {"running", "pending"}
TOKEN_ESTIMATE_FALLBACK_TOTAL = 600

TOOL_BUCKETS = [
    {
        "key": "preAudit",
        "name": "报销预审",
        "color": "var(--theme-primary)",
        "keywords": ("claim", "expense", "reimbursement", "draft", "review"),
    },
    {
        "key": "policyQa",
        "name": "政策问答",
        "color": "var(--chart-blue)",
        "keywords": ("knowledge", "policy", "rag", "wiki", "qa"),
    },
    {
        "key": "invoiceOcr",
        "name": "票据识别",
        "color": "var(--chart-amber)",
        "keywords": ("ocr", "invoice", "receipt", "ticket"),
    },
    {
        "key": "ruleAudit",
        "name": "规则审核",
        "color": "var(--chart-purple)",
        "keywords": ("rule", "risk", "audit", "guard"),
    },
    {
        "key": "employeeLookup",
        "name": "员工查询",
        "color": "var(--success)",
        "keywords": ("employee", "profile", "organization", "department"),
    },
    {
        "key": "diagnosis",
        "name": "异常诊断",
        "color": "var(--danger)",
        "keywords": ("diagnosis", "exception", "error", "fallback"),
    },
]


@dataclass(slots=True)
class _DashboardToolCall:
    id: str
    run_id: str
    tool_type: str | None
    tool_name: str | None
    status: str | None
    duration_ms: int | None
    error_message: str | None
    created_at: datetime | None
    input_tokens: int
    output_tokens: int
    total_tokens: int


@dataclass(slots=True)
class _DashboardRun:
    run_id: str
    agent: str | None
    source: str | None
    user_id: str | None
    status: str | None
    started_at: datetime
    tool_calls: list[_DashboardToolCall] = field(default_factory=list)


class SystemDashboardService:
    def __init__(self, db: Session) -> None:
        self.db = db

    def build_dashboard(self, *, days: int = 7) -> SystemDashboardRead:
        window_days = max(1, min(int(days or 7), 30))
        self._ensure_storage_ready()
        now = datetime.now(UTC)
        start = now - timedelta(days=window_days - 1)
        previous_start = start - timedelta(days=window_days)
        labels = self._date_labels(start.date(), window_days)

        runs = self._fetch_runs(start)
        previous_runs = self._fetch_runs(previous_start, before=start)
        sessions = self._fetch_sessions(start)
        feedback_items = self._fetch_feedback(start)
        tool_calls = [tool for run in runs for tool in run.tool_calls]
        previous_tool_calls = [tool for run in previous_runs for tool in run.tool_calls]
        user_names = self._session_display_names(sessions)

        token_records = self._build_token_records(runs)
        total_tokens = sum(item["total"] for item in token_records)
        previous_tokens = sum(item["total"] for item in self._build_token_records(previous_runs))
        positive_feedback = sum(1 for item in feedback_items if int(item.rating or 0) >= 4)
        negative_feedback = sum(1 for item in feedback_items if int(item.rating or 0) <= 3)
        succeeded_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))
        active_sessions = [item for item in sessions if str(item.status or "") == "active"]
        online_user_count = len(self._unique_session_identities(active_sessions))

        return SystemDashboardRead(
            window_days=window_days,
            generated_at=now.isoformat(),
            has_real_data=bool(runs or sessions or feedback_items),
            totals={
                "toolCalls": len(tool_calls),
                "modelTokens": total_tokens,
                "onlineUsers": online_user_count,
                "avgOnlineMinutes": self._average_session_minutes(sessions, now),
                "executionSuccessRate": self._percent(succeeded_runs, len(runs)),
                "positiveFeedback": positive_feedback,
                "negativeFeedback": negative_feedback,
                "failedRuns": failed_runs,
                "toolCallsChange": self._change_percent(len(tool_calls), len(previous_tool_calls)),
                "modelTokensChange": self._change_percent(total_tokens, previous_tokens),
            },
            agent_daily_ratio=self._agent_daily_ratio(labels, tool_calls),
            login_wave=self._login_wave(sessions, start, now),
            token_daily_wave=self._token_daily_wave(labels, token_records),
            user_token_usage=self._user_token_usage(token_records, user_names),
            accuracy_comparison=self._accuracy_comparison(tool_calls),
            usage_duration_summary=self._usage_duration_summary(sessions, now),
            feedback_summary=self._feedback_summary(feedback_items, len(runs)),
            tool_detail_rows=self._tool_detail_rows(tool_calls, token_records),
        )

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

    def _fetch_runs(self, start: datetime, *, before: datetime | None = None) -> list[_DashboardRun]:
        stmt = (
            select(
                AgentRun.run_id.label("run_id"),
                AgentRun.agent.label("agent"),
                AgentRun.source.label("source"),
                AgentRun.user_id.label("user_id"),
                AgentRun.status.label("run_status"),
                AgentRun.started_at.label("started_at"),
                AgentToolCall.id.label("tool_id"),
                AgentToolCall.run_id.label("tool_run_id"),
                AgentToolCall.tool_type.label("tool_type"),
                AgentToolCall.tool_name.label("tool_name"),
                AgentToolCall.status.label("tool_status"),
                AgentToolCall.duration_ms.label("duration_ms"),
                AgentToolCall.error_message.label("tool_error_message"),
                AgentToolCall.created_at.label("tool_created_at"),
                AgentToolCall.request_json["input_tokens"].as_integer().label("request_input_tokens"),
                AgentToolCall.request_json["prompt_tokens"].as_integer().label("request_prompt_tokens"),
                AgentToolCall.request_json["total_tokens"].as_integer().label("request_total_tokens"),
                AgentToolCall.response_json["input_tokens"].as_integer().label("response_input_tokens"),
                AgentToolCall.response_json["output_tokens"].as_integer().label("response_output_tokens"),
                AgentToolCall.response_json["completion_tokens"].as_integer().label("response_completion_tokens"),
                AgentToolCall.response_json["total_tokens"].as_integer().label("response_total_tokens"),
            )
            .outerjoin(AgentToolCall, AgentToolCall.run_id == AgentRun.run_id)
            .where(AgentRun.started_at >= start)
            .order_by(AgentRun.started_at.asc(), AgentToolCall.created_at.asc())
        )
        if before is not None:
            stmt = stmt.where(AgentRun.started_at < before)

        runs: dict[str, _DashboardRun] = {}
        for row in self.db.execute(stmt).all():
            run = runs.get(row.run_id)
            if run is None:
                run = _DashboardRun(
                    run_id=row.run_id,
                    agent=row.agent,
                    source=row.source,
                    user_id=row.user_id,
                    status=row.run_status,
                    started_at=row.started_at,
                )
                runs[row.run_id] = run

            if row.tool_id is None:
                continue

            input_tokens, output_tokens, total_tokens = self._token_counts_from_row(row)
            run.tool_calls.append(
                _DashboardToolCall(
                    id=row.tool_id,
                    run_id=row.tool_run_id or row.run_id,
                    tool_type=row.tool_type,
                    tool_name=row.tool_name,
                    status=row.tool_status,
                    duration_ms=row.duration_ms,
                    error_message=row.tool_error_message,
                    created_at=row.tool_created_at,
                    input_tokens=input_tokens,
                    output_tokens=output_tokens,
                    total_tokens=total_tokens,
                )
            )

        return list(runs.values())

    def _fetch_sessions(self, start: datetime) -> list[UserSessionMetric]:
        stmt = (
            select(UserSessionMetric)
            .where(
                or_(
                    UserSessionMetric.login_at >= start,
                    UserSessionMetric.logout_at >= start,
                    UserSessionMetric.last_activity_at >= start,
                    UserSessionMetric.status == "active",
                )
            )
            .order_by(UserSessionMetric.login_at.asc())
        )
        return list(self.db.scalars(stmt).all())

    def _fetch_feedback(self, start: datetime) -> list[AgentOperationFeedback]:
        stmt = (
            select(AgentOperationFeedback)
            .where(AgentOperationFeedback.created_at >= start)
            .order_by(AgentOperationFeedback.created_at.asc())
        )
        return list(self.db.scalars(stmt).all())

    def _agent_daily_ratio(
        self,
        labels: list[str],
        tool_calls: list[_DashboardToolCall],
    ) -> dict[str, Any]:
        counts = {bucket["key"]: [0 for _ in labels] for bucket in TOOL_BUCKETS}
        label_index = {label: index for index, label in enumerate(labels)}
        for tool in tool_calls:
            label = self._date_label(tool.created_at)
            if label not in label_index:
                continue
            key = self._tool_bucket(tool)["key"]
            counts[key][label_index[label]] += 1

        ratio_series: dict[str, list[int]] = {bucket["key"]: [] for bucket in TOOL_BUCKETS}
        for index in range(len(labels)):
            total = sum(counts[bucket["key"]][index] for bucket in TOOL_BUCKETS)
            for bucket in TOOL_BUCKETS:
                value = counts[bucket["key"]][index]
                ratio_series[bucket["key"]].append(round((value / total) * 100) if total else 0)

        return {
            "labels": labels,
            "agents": [
                {"key": bucket["key"], "name": bucket["name"], "color": bucket["color"]}
                for bucket in TOOL_BUCKETS
            ],
            "series": ratio_series,
        }

    def _login_wave(
        self,
        sessions: list[UserSessionMetric],
        start: datetime,
        now: datetime,
    ) -> dict[str, Any]:
        labels = [f"{hour:02d}:00" for hour in range(24)]
        online_users = [set[str]() for _ in labels]
        interactions = [0 for _ in labels]
        index = {label: idx for idx, label in enumerate(labels)}
        window_start = self._as_utc(start)
        window_end = self._as_utc(now)

        for session in sessions:
            login_at = self._as_utc(session.login_at)
            end_at = self._session_end_at(session, now)
            if end_at < window_start or login_at > window_end:
                continue

            identity = self._session_identity(session)
            overlap_start = max(login_at, window_start)
            overlap_end = min(end_at, window_end)
            cursor = overlap_start.replace(minute=0, second=0, microsecond=0)
            while cursor <= overlap_end:
                label = f"{cursor.hour:02d}:00"
                online_users[index[label]].add(identity)
                cursor += timedelta(hours=1)

            login_label = f"{login_at.hour:02d}:00"
            if login_at >= window_start and login_label in index:
                interactions[index[login_label]] += max(0, int(session.activity_event_count or 0))

        return {
            "labels": labels,
            "loginUsers": [len(items) for items in online_users],
            "interactions": interactions,
        }

    def _token_daily_wave(self, labels: list[str], records: list[dict[str, Any]]) -> dict[str, Any]:
        input_tokens = [0 for _ in labels]
        output_tokens = [0 for _ in labels]
        total_tokens = [0 for _ in labels]
        index = {label: idx for idx, label in enumerate(labels)}
        for record in records:
            label = record["date"]
            if label not in index:
                continue
            position = index[label]
            input_tokens[position] += record["input"]
            output_tokens[position] += record["output"]
            total_tokens[position] += record["total"]
        return {
            "labels": labels,
            "inputTokens": input_tokens,
            "outputTokens": output_tokens,
            "totalTokens": total_tokens,
        }

    def _user_token_usage(
        self,
        records: list[dict[str, Any]],
        user_names: dict[str, str],
    ) -> list[dict[str, Any]]:
        totals: dict[str, int] = {}
        for record in records:
            user_id = str(record.get("user_id") or "unknown").strip() or "unknown"
            totals[user_id] = totals.get(user_id, 0) + int(record["total"])
        colors = [
            "var(--theme-primary)",
            "var(--chart-blue)",
            "var(--chart-amber)",
            "var(--chart-purple)",
            "var(--success)",
            "var(--danger)",
        ]
        rows = sorted(totals.items(), key=lambda item: item[1], reverse=True)[:6]
        return [
            {
                "name": user_names.get(user_id) or self._short_user_label(user_id),
                "role": user_id if user_id != "unknown" else "未知用户",
                "tokens": value,
                "color": colors[index % len(colors)],
            }
            for index, (user_id, value) in enumerate(rows)
        ]

    def _accuracy_comparison(self, tool_calls: list[_DashboardToolCall]) -> dict[str, Any]:
        correct = {bucket["name"]: 0 for bucket in TOOL_BUCKETS}
        wrong = {bucket["name"]: 0 for bucket in TOOL_BUCKETS}
        for tool in tool_calls:
            name = self._tool_bucket(tool)["name"]
            if self._is_success(tool.status):
                correct[name] += 1
            else:
                wrong[name] += 1
        categories = [bucket["name"] for bucket in TOOL_BUCKETS]
        return {
            "categories": categories,
            "correct": [correct[name] for name in categories],
            "wrong": [wrong[name] for name in categories],
        }

    def _usage_duration_summary(
        self,
        sessions: list[UserSessionMetric],
        now: datetime,
    ) -> dict[str, Any]:
        durations = [self._session_duration_ms(item, now) for item in sessions]
        durations.sort()
        average_ms = int(sum(durations) / len(durations)) if durations else 0
        median_ms = durations[len(durations) // 2] if durations else 0
        peak_ms = max(durations) if durations else 0
        buckets = [
            {"label": "0-10 分钟", "value": 0, "color": "var(--chart-blue)"},
            {"label": "10-30 分钟", "value": 0, "color": "var(--theme-primary)"},
            {"label": "30-60 分钟", "value": 0, "color": "var(--chart-purple)"},
            {"label": "60 分钟以上", "value": 0, "color": "var(--chart-amber)"},
        ]
        for value in durations:
            minutes = value / 60000
            if minutes < 10:
                buckets[0]["value"] += 1
            elif minutes < 30:
                buckets[1]["value"] += 1
            elif minutes < 60:
                buckets[2]["value"] += 1
            else:
                buckets[3]["value"] += 1
        return {
            "average": self._format_minutes(average_ms),
            "median": self._format_minutes(median_ms),
            "peak": self._format_minutes(peak_ms),
            "trend": "实时",
            "rows": buckets,
        }

    def _feedback_summary(
        self,
        feedback_items: list[AgentOperationFeedback],
        run_count: int,
    ) -> list[dict[str, Any]]:
        positive = sum(1 for item in feedback_items if int(item.rating or 0) >= 4)
        negative = sum(1 for item in feedback_items if int(item.rating or 0) <= 3)
        rate = self._percent(len(feedback_items), run_count)
        return [
            {"label": "好评次数", "value": positive, "tone": "success", "icon": "mdi mdi-thumb-up-outline"},
            {"label": "差评次数", "value": negative, "tone": "danger", "icon": "mdi mdi-thumb-down-outline"},
            {"label": "反馈率", "value": f"{rate:.1f}%", "tone": "info", "icon": "mdi mdi-message-processing-outline"},
        ]

    def _tool_detail_rows(
        self,
        tool_calls: list[_DashboardToolCall],
        records: list[dict[str, Any]],
    ) -> list[dict[str, Any]]:
        token_by_tool = {str(record["tool_id"]): int(record["total"]) for record in records}
        rows: list[dict[str, Any]] = []
        for bucket in TOOL_BUCKETS:
            bucket_calls = [tool for tool in tool_calls if self._tool_bucket(tool)["key"] == bucket["key"]]
            if not bucket_calls:
                rows.append(
                    {
                        "name": bucket["name"],
                        "calls": 0,
                        "successRate": 0,
                        "avgLatency": "0.0s",
                        "tokens": 0,
                        "color": bucket["color"],
                    }
                )
                continue
            success = sum(1 for tool in bucket_calls if self._is_success(tool.status))
            avg_ms = sum(max(0, int(tool.duration_ms or 0)) for tool in bucket_calls) / len(bucket_calls)
            tokens = sum(token_by_tool.get(str(tool.id), 0) for tool in bucket_calls)
            rows.append(
                {
                    "name": bucket["name"],
                    "calls": len(bucket_calls),
                    "successRate": round(self._percent(success, len(bucket_calls)), 1),
                    "avgLatency": f"{avg_ms / 1000:.1f}s",
                    "tokens": tokens,
                    "color": bucket["color"],
                }
            )
        return rows

    def _build_token_records(self, runs: list[_DashboardRun]) -> list[dict[str, Any]]:
        records: list[dict[str, Any]] = []
        for run in runs:
            for tool in run.tool_calls:
                input_tokens = int(tool.input_tokens or 0)
                output_tokens = int(tool.output_tokens or 0)
                total = int(tool.total_tokens or input_tokens + output_tokens)
                if total <= 0:
                    total = self._estimate_tool_tokens(tool) if hasattr(tool, "request_json") else 0
                    input_tokens = int(total * 0.62)
                    output_tokens = total - input_tokens
                records.append(
                    {
                        "tool_id": tool.id,
                        "user_id": run.user_id or "",
                        "date": self._date_label(tool.created_at or run.started_at),
                        "input": input_tokens,
                        "output": output_tokens,
                        "total": total,
                    }
                )
        return records

    def _token_counts_from_row(self, row: Any) -> tuple[int, int, int]:
        input_tokens = self._first_positive_int(
            row.request_input_tokens,
            row.request_prompt_tokens,
            row.response_input_tokens,
        )
        output_tokens = self._first_positive_int(
            row.response_output_tokens,
            row.response_completion_tokens,
        )
        total_tokens = self._first_positive_int(
            row.request_total_tokens,
            row.response_total_tokens,
        )

        if total_tokens and not input_tokens and not output_tokens:
            input_tokens = int(total_tokens * 0.62)
            output_tokens = total_tokens - input_tokens

        if input_tokens + output_tokens <= 0 and total_tokens <= 0:
            total_tokens = TOKEN_ESTIMATE_FALLBACK_TOTAL
            input_tokens = int(total_tokens * 0.62)
            output_tokens = total_tokens - input_tokens

        if total_tokens <= 0:
            total_tokens = input_tokens + output_tokens

        return input_tokens, output_tokens, total_tokens

    @staticmethod
    def _first_positive_int(*values: Any) -> int:
        for value in values:
            if isinstance(value, (int, float)) and value > 0:
                return int(value)
        return 0

    def _extract_tool_tokens(self, tool: AgentToolCall) -> tuple[int, int]:
        payload = {
            "request": tool.request_json or {},
            "response": tool.response_json or {},
        }
        input_tokens = self._first_int(payload, ("input_tokens", "prompt_tokens"))
        output_tokens = self._first_int(payload, ("output_tokens", "completion_tokens"))
        total_tokens = self._first_int(payload, ("total_tokens", "tokens", "token_count"))
        if total_tokens and not input_tokens and not output_tokens:
            input_tokens = int(total_tokens * 0.62)
            output_tokens = total_tokens - input_tokens
        return input_tokens, output_tokens

    def _estimate_tool_tokens(self, tool: AgentToolCall) -> int:
        payload = {
            "request": tool.request_json,
            "response": tool.response_json,
            "error": tool.error_message,
        }
        text = json.dumps(payload, ensure_ascii=False, default=str)
        return max(0, len(text) // 4)

    def _first_int(self, 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 = self._first_int(value, keys)
                if found:
                    return found
        if isinstance(payload, list):
            for value in payload:
                found = self._first_int(value, keys)
                if found:
                    return found
        return 0

    def _tool_bucket(self, tool: AgentToolCall | _DashboardToolCall) -> dict[str, Any]:
        text = f"{tool.tool_type or ''} {tool.tool_name or ''}".lower()
        if self._is_failed(tool.status) and ("timeout" in text or tool.error_message):
            return TOOL_BUCKETS[-1]
        for bucket in TOOL_BUCKETS:
            if any(keyword in text for keyword in bucket["keywords"]):
                return bucket
        return TOOL_BUCKETS[0]

    def _session_display_names(self, sessions: list[UserSessionMetric]) -> dict[str, str]:
        names: dict[str, str] = {}
        for item in sessions:
            display_name = str(item.display_name or item.username or item.email or "").strip()
            for key in {item.username, item.email, item.employee_no, item.display_name}:
                normalized = str(key or "").strip()
                if normalized and display_name:
                    names[normalized] = display_name
        return names

    def _average_session_minutes(self, sessions: list[UserSessionMetric], now: datetime) -> float:
        if not sessions:
            return 0.0
        durations = [self._session_duration_ms(item, now) for item in sessions]
        return round((sum(durations) / len(durations)) / 60000, 1)

    def _session_duration_ms(self, session: UserSessionMetric, now: datetime) -> int:
        if int(session.duration_ms or 0) > 0:
            return max(0, int(session.duration_ms or 0))
        login_at = self._as_utc(session.login_at)
        end_at = self._session_end_at(session, now)
        try:
            return max(0, min(int((end_at - login_at).total_seconds() * 1000), 24 * 60 * 60 * 1000))
        except TypeError:
            return 0

    def _session_end_at(self, session: UserSessionMetric, now: datetime) -> datetime:
        if str(session.status or "").strip().lower() == "active":
            return self._as_utc(now)
        return self._as_utc(session.logout_at or session.last_activity_at or session.login_at or now)

    def _unique_session_identities(self, sessions: list[UserSessionMetric]) -> set[str]:
        return {self._session_identity(item) for item in sessions}

    @staticmethod
    def _session_identity(session: UserSessionMetric) -> str:
        for value in (session.username, session.email, session.employee_no, session.display_name):
            normalized = str(value or "").strip().lower()
            if normalized:
                return normalized
        return str(session.session_id or "").strip().lower()

    @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 SystemDashboardService._as_utc(value).strftime("%m-%d")

    @staticmethod
    def _format_minutes(duration_ms: int) -> str:
        return f"{duration_ms / 60000:.1f} 分钟"

    @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 _change_percent(value: int | float, previous: int | float) -> float:
        if not previous:
            return 0.0
        return round(((float(value) - float(previous)) / float(previous)) * 100, 1)

    @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 _short_user_label(user_id: str) -> str:
        normalized = str(user_id or "").strip()
        if not normalized or normalized == "unknown":
            return "未知用户"
        return normalized.split("@", 1)[0]
