from __future__ import annotations

import json
import os
import time
from collections import Counter, deque
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional

from modules.user_manager import UserWorkspace
from modules.usage_tracker import QUOTA_DEFAULTS
from utils.conversation_manager import ConversationManager
from .context import get_or_create_usage_tracker

from config import PROJECT_MAX_STORAGE_MB, PROJECT_MAX_STORAGE_BYTES, UPLOAD_SCAN_LOG_SUBDIR, LOGS_DIR
from .state import (
    RECENT_UPLOAD_EVENT_LIMIT,
    RECENT_UPLOAD_FEED_LIMIT,
    user_manager,
    container_manager,
    get_last_active_ts,
)


def calculate_directory_size(root: Path) -> int:
    if not root.exists():
        return 0
    total = 0
    stack = [root]
    while stack:
        current = stack.pop()
        try:
            with os.scandir(current) as iterator:
                for entry in iterator:
                    try:
                        if entry.is_symlink():
                            continue
                        if entry.is_file(follow_symlinks=False):
                            total += entry.stat(follow_symlinks=False).st_size
                        elif entry.is_dir(follow_symlinks=False):
                            stack.append(Path(entry.path))
                    except (OSError, FileNotFoundError, PermissionError):
                        continue
        except (NotADirectoryError, FileNotFoundError, PermissionError, OSError):
            continue
    return total

def iso_datetime_from_epoch(epoch: Optional[float]) -> Optional[str]:
    if not epoch:
        return None
    try:
        return datetime.utcfromtimestamp(epoch).replace(microsecond=0).isoformat() + "Z"
    except (ValueError, OSError):
        return None

def compute_workspace_storage(workspace: UserWorkspace) -> Dict[str, Any]:
    project_bytes = calculate_directory_size(workspace.project_path)
    data_bytes = calculate_directory_size(workspace.data_dir)
    logs_bytes = calculate_directory_size(workspace.logs_dir)
    quarantine_bytes = calculate_directory_size(workspace.quarantine_dir)
    uploads_bytes = calculate_directory_size(workspace.uploads_dir)
    backups_bytes = calculate_directory_size(workspace.data_dir / "backups")
    usage_percent = None
    if PROJECT_MAX_STORAGE_BYTES:
        usage_percent = round(project_bytes / PROJECT_MAX_STORAGE_BYTES * 100, 2) if project_bytes else 0.0
    status = "ok"
    if usage_percent is not None:
        if usage_percent >= 95:
            status = "critical"
        elif usage_percent >= 80:
            status = "warning"
    return {
        "project_bytes": project_bytes,
        "data_bytes": data_bytes,
        "logs_bytes": logs_bytes,
        "quarantine_bytes": quarantine_bytes,
        "uploads_bytes": uploads_bytes,
        "backups_bytes": backups_bytes,
        "total_bytes": project_bytes + data_bytes + logs_bytes + quarantine_bytes,
        "limit_bytes": PROJECT_MAX_STORAGE_BYTES,
        "usage_percent": usage_percent,
        "status": status,
    }

def collect_usage_snapshot(username: str, workspace: UserWorkspace, role: Optional[str]) -> Dict[str, Any]:
    tracker = get_or_create_usage_tracker(username, workspace)
    if not tracker:
        # 兜底：理论上网页用户应始终有 tracker；这里避免因异常导致整个 dashboard 失败
        snapshot: Dict[str, Any] = {}
        for metric in ("fast", "thinking", "search"):
            default_limit = QUOTA_DEFAULTS.get("default", {}).get(metric, {}).get("limit", 0)
            snapshot[metric] = {
                "count": 0,
                "window_start": None,
                "reset_at": None,
                "limit": default_limit,
            }
        snapshot["role"] = role or "user"
        return snapshot
    stats = tracker.get_stats()
    quotas = stats.get("quotas") or {}
    windows = stats.get("windows") or {}
    snapshot: Dict[str, Any] = {}
    for metric in ("fast", "thinking", "search"):
        window_meta = windows.get(metric) or {}
        quota_meta = quotas.get(metric) or {}
        default_limit = QUOTA_DEFAULTS.get("default", {}).get(metric, {}).get("limit", 0)
        snapshot[metric] = {
            "count": int(window_meta.get("count", 0) or 0),
            "window_start": window_meta.get("window_start"),
            "reset_at": window_meta.get("reset_at") or quota_meta.get("reset_at"),
            "limit": quota_meta.get("limit", default_limit),
        }
    snapshot["role"] = role or quotas.get("role") or "user"
    return snapshot

def _read_token_totals_file(workspace: UserWorkspace) -> Dict[str, int]:
    path = workspace.data_dir / "token_totals.json"
    if not path.exists():
        return {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
    try:
        with open(path, 'r', encoding='utf-8') as fh:
            payload = json.load(fh) or {}
        input_tokens = int(payload.get("input_tokens") or payload.get("total_input_tokens") or 0)
        output_tokens = int(payload.get("output_tokens") or payload.get("total_output_tokens") or 0)
        total_tokens = int(payload.get("total_tokens") or (input_tokens + output_tokens))
        return {
            "input_tokens": input_tokens,
            "output_tokens": output_tokens,
            "total_tokens": total_tokens,
        }
    except (OSError, json.JSONDecodeError, ValueError) as exc:
        print(f"[admin] 解析 token_totals.json 失败 ({workspace.username}): {exc}")
        return {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}

def _collect_conversation_token_totals(workspace: UserWorkspace) -> Dict[str, int]:
    try:
        manager = ConversationManager(base_dir=workspace.data_dir, project_path=str(workspace.project_path))
        stats = manager.get_statistics() or {}
        token_stats = stats.get("token_statistics") or {}
        input_tokens = int(token_stats.get("total_input_tokens") or 0)
        output_tokens = int(token_stats.get("total_output_tokens") or 0)
        total_tokens = int(token_stats.get("total_tokens") or (input_tokens + output_tokens))
        return {
            "input_tokens": input_tokens,
            "output_tokens": output_tokens,
            "total_tokens": total_tokens,
        }
    except Exception as exc:
        print(f"[admin] 读取 legacy token 统计失败 ({workspace.username}): {exc}")
        return {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}

def collect_user_token_statistics(workspace: UserWorkspace) -> Dict[str, int]:
    """汇总单个用户在所有对话中的token累计数据。"""
    file_totals = _read_token_totals_file(workspace)
    legacy_totals = _collect_conversation_token_totals(workspace)
    return {
        "input_tokens": max(file_totals["input_tokens"], legacy_totals["input_tokens"]),
        "output_tokens": max(file_totals["output_tokens"], legacy_totals["output_tokens"]),
        "total_tokens": max(file_totals["total_tokens"], legacy_totals["total_tokens"]),
    }

def compute_usage_leaders(users: List[Dict[str, Any]], metric: str, top_n: int = 5) -> List[Dict[str, Any]]:
    ranked = sorted(
        (
            {
                "username": entry["username"],
                "count": entry.get("usage", {}).get(metric, {}).get("count", 0),
                "limit": entry.get("usage", {}).get(metric, {}).get("limit"),
            }
            for entry in users
        ),
        key=lambda item: item["count"],
        reverse=True,
    )
    return [row for row in ranked[:top_n] if row["count"]]

def collect_user_snapshots(handle_map: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
    user_map = user_manager.list_users()
    items: List[Dict[str, Any]] = []
    role_counter: Counter = Counter()
    usage_totals = {"fast": 0, "thinking": 0, "search": 0}
    token_totals = {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
    storage_total_bytes = 0
    quarantine_total_bytes = 0
    now = time.time()

    for username, record in user_map.items():
        workspace = user_manager.ensure_user_workspace(username)
        storage = compute_workspace_storage(workspace)
        usage = collect_usage_snapshot(username, workspace, record.role)
        tokens = collect_user_token_statistics(workspace)
        storage_total_bytes += storage["total_bytes"]
        quarantine_total_bytes += storage["quarantine_bytes"]
        for metric in usage_totals:
            usage_totals[metric] += usage.get(metric, {}).get("count", 0)
        for key in token_totals:
            token_totals[key] += tokens.get(key, 0)
        normalized_role = (record.role or "user").lower()
        role_counter[normalized_role] += 1
        handle = handle_map.get(username)
        handle_last = handle.get("last_active") if handle else None
        last_active = get_last_active_ts(username, handle_last)
        idle_seconds = max(0.0, now - last_active) if last_active else None
        items.append({
            "username": username,
            "email": record.email,
            "role": record.role or "user",
            "created_at": record.created_at,
            "invite_code": record.invite_code,
            "storage": storage,
            "usage": usage,
            "tokens": tokens,
            "workspace": {
                "project_path": str(workspace.project_path),
                "data_dir": str(workspace.data_dir),
                "logs_dir": str(workspace.logs_dir),
                "uploads_dir": str(workspace.uploads_dir),
            },
            "status": {
                "online": handle is not None,
                "container_mode": handle.get("mode") if handle else None,
                "last_active": iso_datetime_from_epoch(last_active),
                "idle_seconds": idle_seconds,
            },
        })

    items.sort(key=lambda entry: entry["username"])
    return {
        "items": items,
        "roles": dict(role_counter),
        "usage_totals": usage_totals,
        "token_totals": token_totals,
        "storage_total_bytes": storage_total_bytes,
        "quarantine_total_bytes": quarantine_total_bytes,
        "active_users": sum(1 for entry in items if entry["status"]["online"]),
        "total_users": len(items),
    }

def collect_container_snapshots(handle_map: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
    items: List[Dict[str, Any]] = []
    cpu_values: List[float] = []
    mem_percent_values: List[float] = []
    total_mem_used = 0
    total_mem_limit = 0
    total_net_rx = 0
    total_net_tx = 0
    docker_count = 0
    failure_count = 0
    now = time.time()

    for username, handle in sorted(handle_map.items()):
        try:
            status = container_manager.get_container_status(username)
        except Exception as exc:
            status = {
                "username": username,
                "mode": handle.get("mode"),
                "error": str(exc),
                "workspace_path": handle.get("workspace_path"),
            }
        stats = status.get("stats") or {}
        state = status.get("state") or {}
        if status.get("mode") == "docker":
            docker_count += 1
        last_active = get_last_active_ts(username, handle.get("last_active"))
        idle_seconds = max(0.0, now - last_active) if last_active else None
        entry = {
            "username": username,
            "mode": status.get("mode", handle.get("mode")),
            "workspace_path": status.get("workspace_path") or handle.get("workspace_path"),
            "container_name": status.get("container_name") or handle.get("container_name"),
            "created_at": iso_datetime_from_epoch(status.get("created_at") or handle.get("created_at")),
            "last_active": iso_datetime_from_epoch(status.get("last_active") or last_active),
            "idle_seconds": idle_seconds,
            "stats": stats,
            "state": state,
            "error": status.get("error"),
        }
        if entry["error"] or (state and not state.get("running", True)):
            failure_count += 1
        mem_info = stats.get("memory") or {}
        net_info = stats.get("net_io") or {}
        cpu_val = stats.get("cpu_percent")
        mem_percent = mem_info.get("percent")
        mem_used = mem_info.get("used_bytes")
        mem_limit = mem_info.get("limit_bytes")
        rx_bytes = net_info.get("rx_bytes")
        tx_bytes = net_info.get("tx_bytes")
        if isinstance(cpu_val, (int, float)):
            cpu_values.append(cpu_val)
        if isinstance(mem_percent, (int, float)):
            mem_percent_values.append(mem_percent)
        if isinstance(mem_used, (int, float)):
            total_mem_used += mem_used
        if isinstance(mem_limit, (int, float)):
            total_mem_limit += mem_limit
        if isinstance(rx_bytes, (int, float)):
            total_net_rx += rx_bytes
        if isinstance(tx_bytes, (int, float)):
            total_net_tx += tx_bytes
        items.append(entry)

    active_total = len(handle_map)
    summary = {
        "active": active_total,
        "docker": docker_count,
        "host": active_total - docker_count,
        "issues": failure_count,
        "max_containers": container_manager.max_containers,
        "available_slots": max(0, container_manager.max_containers - active_total) if container_manager.max_containers > 0 else None,
        "avg_cpu_percent": round(sum(cpu_values) / len(cpu_values), 2) if cpu_values else None,
        "avg_mem_percent": round(sum(mem_percent_values) / len(mem_percent_values), 2) if mem_percent_values else None,
        "total_mem_used_bytes": total_mem_used,
        "total_mem_limit_bytes": total_mem_limit,
        "net_rx_bytes": total_net_rx,
        "net_tx_bytes": total_net_tx,
    }
    return {"items": items, "summary": summary}

def parse_upload_line(line: str) -> Optional[Dict[str, Any]]:
    marker = "UPLOAD_AUDIT "
    idx = line.find(marker)
    if idx == -1:
        return None
    payload = line[idx + len(marker):].strip()
    try:
        data = json.loads(payload)
    except json.JSONDecodeError:
        return None
    timestamp_value = data.get("timestamp")
    timestamp_dt = None
    if isinstance(timestamp_value, str):
        try:
            timestamp_dt = datetime.fromisoformat(timestamp_value)
        except ValueError:
            timestamp_dt = None
    data["_dt"] = timestamp_dt
    return data

def collect_upload_events(limit: int = RECENT_UPLOAD_EVENT_LIMIT) -> List[Dict[str, Any]]:
    base_dir = (Path(LOGS_DIR).expanduser().resolve() / UPLOAD_SCAN_LOG_SUBDIR).resolve()
    events: List[Dict[str, Any]] = []
    if not base_dir.exists():
        return []
    for log_file in sorted(base_dir.glob('*.log')):
        buffer: deque = deque(maxlen=limit)
        try:
            with open(log_file, 'r', encoding='utf-8') as fh:
                for line in fh:
                    if 'UPLOAD_AUDIT' not in line:
                        continue
                    buffer.append(line.strip())
        except OSError:
            continue
        for raw in buffer:
            event = parse_upload_line(raw)
            if event:
                events.append(event)
    events.sort(key=lambda item: item.get('_dt') or datetime.min, reverse=True)
    return events[:limit]

def summarize_upload_events(events: List[Dict[str, Any]], quarantine_total_bytes: int) -> Dict[str, Any]:
    now = datetime.utcnow()
    cutoff = now - timedelta(hours=24)
    last_24h = [evt for evt in events if evt.get('_dt') and evt['_dt'] >= cutoff]
    accepted_24h = sum(1 for evt in last_24h if evt.get('accepted'))
    blocked_24h = len(last_24h) - accepted_24h
    skipped_24h = sum(1 for evt in last_24h if ((evt.get('scan') or {}).get('status') == 'skipped'))
    source_counter = Counter((evt.get('source') or 'unknown') for evt in events)
    sanitized_events: List[Dict[str, Any]] = []
    for evt in events[:RECENT_UPLOAD_FEED_LIMIT]:
        sanitized_events.append({k: v for k, v in evt.items() if k != '_dt'})
    return {
        "stats": {
            "total_tracked": len(events),
            "last_24h": len(last_24h),
            "accepted_last_24h": accepted_24h,
            "blocked_last_24h": blocked_24h,
            "skipped_scan_last_24h": skipped_24h,
            "quarantine_bytes": quarantine_total_bytes,
        },
        "recent_events": sanitized_events,
        "sources": [{"source": src, "count": count} for src, count in source_counter.most_common()],
    }

def summarize_invite_codes(codes: List[Dict[str, Any]]) -> Dict[str, int]:
    active = consumed = unlimited = 0
    for code in codes:
        remaining = code.get('remaining')
        if remaining is None:
            unlimited += 1
        elif remaining > 0:
            active += 1
        else:
            consumed += 1
    return {
        "total": len(codes),
        "active": active,
        "consumed": consumed,
        "unlimited": unlimited,
    }

def build_admin_dashboard_snapshot() -> Dict[str, Any]:
    handle_map = container_manager.list_containers()
    user_data = collect_user_snapshots(handle_map)
    container_data = collect_container_snapshots(handle_map)
    invite_codes = user_manager.list_invite_codes()
    upload_events = collect_upload_events()
    uploads_summary = summarize_upload_events(upload_events, user_data['quarantine_total_bytes'])
    overview = {
        "generated_at": datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
        "totals": {
            "users": user_data['total_users'],
            "active_users": user_data['active_users'],
            "containers_active": container_data['summary']['active'],
            "containers_max": container_data['summary']['max_containers'],
            "available_container_slots": container_data['summary']['available_slots'],
        },
        "roles": user_data['roles'],
        "usage_totals": user_data['usage_totals'],
        "token_totals": user_data['token_totals'],
        "usage_leaders": {
            metric: compute_usage_leaders(user_data['items'], metric)
            for metric in ("fast", "thinking", "search")
        },
        "storage": {
            "total_bytes": user_data['storage_total_bytes'],
            "per_user_limit_bytes": PROJECT_MAX_STORAGE_BYTES,
            "project_max_mb": PROJECT_MAX_STORAGE_MB,
            "warning_users": [
                {
                    "username": entry['username'],
                    "usage_percent": entry['storage']['usage_percent'],
                    "status": entry['storage']['status'],
                }
                for entry in user_data['items']
                if entry['storage']['status'] != 'ok'
            ],
        },
        "containers": container_data['summary'],
        "invites": summarize_invite_codes(invite_codes),
        "uploads": uploads_summary['stats'],
    }
    return {
        "generated_at": overview['generated_at'],
        "overview": overview,
        "users": user_data['items'],
        "containers": container_data['items'],
        "invites": {
            "summary": summarize_invite_codes(invite_codes),
            "codes": invite_codes,
        },
        "uploads": uploads_summary,
    }
