"""
Vincent Dashboard โ€” AI assistant operational state monitor.
Shows Vincent's own state: memory, skills, sessions, calibration, grounding, cron jobs, services.
"""

import json
import os
import re
import sqlite3
import subprocess
import time
from datetime import datetime, timezone
from pathlib import Path

from flask import Flask, jsonify, render_template, make_response, request

app = Flask(__name__)


@app.after_request
def no_cache(response):
    response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    response.headers["Pragma"] = "no-cache"
    response.headers["Expires"] = "0"
    return response

HERMES_HOME = Path(os.path.expanduser("~/.hermes"))
STATE_DB = HERMES_HOME / "state.db"
SESSION_STATE = HERMES_HOME / "session_state.json"
MEMORY_FILE = HERMES_HOME / "memories" / "MEMORY.md"
USER_FILE = HERMES_HOME / "memories" / "USER.md"
CALIBRATION_FILE = HERMES_HOME / "calibration" / "predictions.json"
CONFIG_FILE = HERMES_HOME / "config.yaml"
CRON_JOBS_FILE = HERMES_HOME / "cron" / "jobs.json"
SKILLS_DIR = HERMES_HOME / "skills"


def get_session_state():
    """Read session continuity state."""
    try:
        with open(SESSION_STATE) as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return {"focus": "None", "next": "None", "context": "Idle"}


def get_memory_stats():
    """Read memory file sizes and estimate usage."""
    stats = {"memory": {}, "user": {}}
    for name, path in [("memory", MEMORY_FILE), ("user", USER_FILE)]:
        try:
            with open(path) as f:
                content = f.read()
            stats[name] = {
                "exists": True,
                "size_bytes": os.path.getsize(path),
                "char_count": len(content),
                "entries": content.count("ยง"),
            }
        except FileNotFoundError:
            stats[name] = {"exists": False, "size_bytes": 0, "char_count": 0, "entries": 0}
    return stats


def get_session_stats():
    """Query state.db for session statistics."""
    try:
        conn = sqlite3.connect(str(STATE_DB))
        conn.row_factory = sqlite3.Row
        cur = conn.cursor()

        # Total sessions
        cur.execute("SELECT COUNT(*) as total FROM sessions WHERE archived = 0")
        total = cur.fetchone()["total"]

        # Total tokens
        cur.execute(
            "SELECT SUM(input_tokens) as inp, SUM(output_tokens) as out, "
            "SUM(cache_read_tokens) as cr, SUM(cache_write_tokens) as cw, "
            "SUM(reasoning_tokens) as reason FROM sessions WHERE archived = 0"
        )
        tokens = cur.fetchone()

        # Total cost
        cur.execute(
            "SELECT SUM(estimated_cost_usd) as cost FROM sessions "
            "WHERE archived = 0 AND estimated_cost_usd IS NOT NULL"
        )
        cost_row = cur.fetchone()

        # Recent sessions (last 10)
        cur.execute(
            "SELECT id, title, source, model, started_at, message_count, "
            "input_tokens, output_tokens, estimated_cost_usd, end_reason "
            "FROM sessions WHERE archived = 0 "
            "ORDER BY started_at DESC LIMIT 10"
        )
        recent = []
        for row in cur.fetchall():
            ts = datetime.fromtimestamp(row["started_at"], tz=timezone.utc)
            recent.append({
                "id": row["id"][:16],
                "title": row["title"] or "Untitled",
                "source": row["source"],
                "model": (row["model"] or "").replace("-UD-Q4_K_XL.gguf", "").replace(".gguf", ""),
                "started": ts.strftime("%Y-%m-%d %H:%M"),
                "messages": row["message_count"] or 0,
                "input_tokens": row["input_tokens"] or 0,
                "output_tokens": row["output_tokens"] or 0,
                "cost": row["estimated_cost_usd"] or 0,
                "end_reason": row["end_reason"] or "",
            })

        # Sessions today
        today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0).timestamp()
        cur.execute(
            "SELECT COUNT(*) as cnt FROM sessions "
            "WHERE started_at >= ? AND archived = 0", (today_start,)
        )
        today_count = cur.fetchone()["cnt"]

        conn.close()

        return {
            "total": total,
            "today": today_count,
            "tokens": {
                "input": tokens["inp"] or 0,
                "output": tokens["out"] or 0,
                "cache_read": tokens["cr"] or 0,
                "cache_write": tokens["cw"] or 0,
                "reasoning": tokens["reason"] or 0,
            },
            "cost_usd": cost_row["cost"] or 0,
            "recent": recent,
        }
    except Exception as e:
        return {"error": str(e)}


def get_calibration_stats():
    """Read calibration prediction tracking."""
    try:
        with open(CALIBRATION_FILE) as f:
            predictions = json.load(f)
        total = len(predictions)
        correct = sum(1 for p in predictions if p.get("status") == "correct")
        incorrect = sum(1 for p in predictions if p.get("status") == "incorrect")
        pending = total - correct - incorrect
        accuracy = (correct / total * 100) if total > 0 else 0
        return {
            "total": total,
            "correct": correct,
            "incorrect": incorrect,
            "pending": pending,
            "accuracy": accuracy,
            "recent": predictions[-5:] if predictions else [],
        }
    except (FileNotFoundError, json.JSONDecodeError) as e:
        return {"error": str(e)}


def get_system_health():
    """Read system health from /proc."""
    health = {}

    # CPU load
    try:
        with open("/proc/loadavg") as f:
            parts = f.read().split()
            health["load"] = {
                "1m": float(parts[0]),
                "5m": float(parts[1]),
                "15m": float(parts[2]),
            }
    except Exception:
        health["load"] = {}

    # Memory
    try:
        with open("/proc/meminfo") as f:
            meminfo = {}
            for line in f:
                parts = line.split()
                key = parts[0].rstrip(":")
                val = int(parts[1])  # kB
                meminfo[key] = val
        total_mb = meminfo.get("MemTotal", 0) / 1024
        avail_mb = meminfo.get("MemAvailable", 0) / 1024
        used_mb = total_mb - avail_mb
        health["memory"] = {
            "total_gb": round(total_mb / 1024, 1),
            "used_gb": round(used_mb / 1024, 1),
            "percent": round(used_mb / total_mb * 100, 1) if total_mb > 0 else 0,
        }
    except Exception:
        health["memory"] = {}

    # Disk โ€” check both SSD and HDD
    disks = []
    for mount, label in [("/", "SSD"), ("/mnt/storage", "HDD")]:
        try:
            stat = os.statvfs(mount)
            total_gb = stat.f_blocks * stat.f_frsize / (1024**3)
            free_gb = stat.f_bavail * stat.f_frsize / (1024**3)
            used_gb = total_gb - free_gb
            disks.append({
                "label": label,
                "mount": mount,
                "total_gb": round(total_gb, 0),
                "used_gb": round(used_gb, 0),
                "free_gb": round(free_gb, 0),
                "percent": round(used_gb / total_gb * 100, 1) if total_gb > 0 else 0,
            })
        except Exception:
            pass
    health["disk"] = disks

    # CPU cores
    try:
        health["cpu_cores"] = os.cpu_count() or 0
    except Exception:
        health["cpu_cores"] = 0

    # CPU usage % (sample /proc/stat twice)
    try:
        def read_cpu_stat():
            with open("/proc/stat") as f:
                parts = f.readline().split()
            vals = [int(v) for v in parts[1:]]
            idle = vals[3] + (vals[4] if len(vals) > 4 else 0)
            total = sum(vals[:8])
            return idle, total

        idle1, total1 = read_cpu_stat()
        time.sleep(0.3)
        idle2, total2 = read_cpu_stat()

        d_total = total2 - total1
        d_idle = idle2 - idle1
        cpu_pct = ((d_total - d_idle) / d_total * 100) if d_total > 0 else 0
        health["cpu_usage_pct"] = round(cpu_pct, 1)
    except Exception:
        health["cpu_usage_pct"] = 0

    return health


def get_gpu_stats():
    """Read GPU stats from nvidia-smi."""
    gpu = {}
    try:
        result = subprocess.run(
            [
                "nvidia-smi",
                "--query-gpu=temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.used,memory.free,name",
                "--format=csv,noheader",
            ],
            capture_output=True,
            text=True,
            timeout=10,
        )
        if result.returncode == 0:
            parts = [p.strip().replace(" MiB", "").replace(" %", "").strip() for p in result.stdout.strip().split(",")]
            gpu = {
                "name": parts[6] if len(parts) > 6 else "Unknown",
                "temp_c": int(parts[0]) if parts[0].isdigit() else 0,
                "util_pct": int(parts[1]) if parts[1].isdigit() else 0,
                "mem_util_pct": int(parts[2]) if parts[2].isdigit() else 0,
                "mem_total_mb": int(parts[3]) if parts[3].isdigit() else 0,
                "mem_used_mb": int(parts[4]) if parts[4].isdigit() else 0,
                "mem_free_mb": int(parts[5]) if parts[5].isdigit() else 0,
                # Calculate actual VRAM usage ratio (model-loaded memory)
                "mem_used_pct": round(int(parts[4]) / int(parts[3]) * 100) if (len(parts) > 4 and parts[3].isdigit() and int(parts[3]) > 0) else 0,
            }
    except Exception:
        pass
    return gpu


def get_cron_jobs():
    """Read cron jobs dynamically from jobs.json."""
    try:
        with open(CRON_JOBS_FILE) as f:
            data = json.load(f)

        # Handle both dict wrapper and bare list formats
        if isinstance(data, dict):
            jobs = data.get("jobs", [])
        elif isinstance(data, list):
            jobs = data
        else:
            return {"jobs": [], "total": 0}

        enabled = []
        for job in jobs:
            if isinstance(job, dict) and not job.get("enabled", True):
                continue
            sched = job.get("schedule", "unknown")
            if isinstance(sched, dict):
                sched = sched.get("display", str(sched))
            enabled.append({
                "name": job.get("name", "Unnamed"),
                "id": job.get("id", "")[:8],
                "schedule": sched,
                "next_run": job.get("next_run_at", "")[:19].replace("T", " ").replace("-04:00", "") if job.get("next_run_at") else "โ€”",
                "last_run": job.get("last_run_at", "")[:19].replace("T", " ").replace("-04:00", "") if job.get("last_run_at") else "โ€”",
                "last_status": job.get("last_status", "unknown"),
            })

        return {"jobs": enabled, "total": len(enabled)}
    except (FileNotFoundError, json.JSONDecodeError) as e:
        return {"jobs": [], "total": 0, "error": str(e)}


def get_skills_count():
    """Count skills dynamically."""
    try:
        count = 0
        for item in SKILLS_DIR.rglob("SKILL.md"):
            count += 1
        return count
    except Exception:
        return 0


def get_services():
    """Check known services."""
    services = {
        "Qwen3.6-27B (Main)": {"port": 8080, "pid": None, "running": False},
        "Qwen2.5-3B Supervisor": {"port": 8085, "pid": None, "running": False},
        "Qwen3.5-4B Builder": {"port": 8086, "pid": None, "running": False},
        "Freqtrade": {"port": 8081, "pid": None, "running": False},
        "AgentForms Relay": {"port": 5060, "pid": None, "running": False},
        "AgentForms Worker": {"port": 5070, "pid": None, "running": False},
        "Cloudflare Tunnel": {"port": 5051, "pid": None, "running": False},
        "Home Assistant": {"port": 8123, "pid": None, "running": False},
        "SSH": {"port": 2222, "pid": None, "running": False},
    }

    try:
        import socket
        for name, svc in services.items():
            port = svc["port"]
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.settimeout(1)
            try:
                s.connect(("127.0.0.1", port))
                svc["running"] = True
            except (socket.timeout, socket.error, ConnectionRefusedError):
                svc["running"] = False
            finally:
                s.close()
    except:
        pass

    return services


def get_config():
    """Read basic config info."""
    config = {}
    try:
        with open(CONFIG_FILE) as f:
            content = f.read()
        # Extract model info
        for line in content.split("\n"):
            line = line.strip()
            if line.startswith("model:"):
                config["model"] = line.split(":", 1)[1].strip().strip("'\"")
            elif line.startswith("provider:"):
                config["provider"] = line.split(":", 1)[1].strip().strip("'\"")
    except FileNotFoundError:
        pass
    return config


def fmt_size(size_bytes):
    """Format bytes to human readable."""
    if size_bytes < 1024:
        return f"{size_bytes}B"
    elif size_bytes < 1024**2:
        return f"{size_bytes / 1024:.1f}K"
    elif size_bytes < 1024**3:
        return f"{size_bytes / 1024**2:.1f}M"
    elif size_bytes < 1024**4:
        return f"{size_bytes / 1024**3:.1f}G"
    else:
        return f"{size_bytes / 1024**4:.1f}T"


def list_directory(path_str):
    """List directory contents with metadata. Read-only browsing only."""
    # Normalize path
    path = Path(path_str if path_str else "/").expanduser().resolve()

    # Security: only allow paths that exist
    if not path.is_dir():
        return {"error": f"Not a directory or does not exist: {path_str}", "path": str(path)}

    entries = []
    try:
        for item in sorted(path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
            try:
                stat = item.stat()
                is_dir = item.is_dir()
                size = stat.st_size if not is_dir else 0
                # Get total size for directories (top-level only, no recursion)
                mod_time = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)

                entry = {
                    "name": item.name,
                    "path": str(item),
                    "is_dir": is_dir,
                    "size": size,
                    "size_str": fmt_size(size) if not is_dir else "โ€”",
                    "modified": mod_time.strftime("%Y-%m-%d %H:%M"),
                    "permissions": oct(stat.st_mode)[-3:],
                }
                entries.append(entry)
            except (PermissionError, OSError):
                # Include entry with limited info
                entries.append({
                    "name": item.name,
                    "path": str(item),
                    "is_dir": item.is_dir() if not item.is_symlink() else False,
                    "size": 0,
                    "size_str": "โ€”",
                    "modified": "โ€”",
                    "permissions": "---",
                    "error": "Permission denied",
                })
    except PermissionError:
        return {"error": f"Permission denied: {path_str}", "path": str(path), "entries": []}

    # Count subdirectories and files
    dirs = sum(1 for e in entries if e["is_dir"])
    files = len(entries) - dirs

    return {
        "path": str(path),
        "display_path": path_str if path_str and path_str != "/" else "/",
        "dirs": dirs,
        "files": files,
        "entries": entries,
    }


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/api/fs")
def api_fs():
    """File system explorer โ€” list directory contents."""
    path = request.args.get("path", "/")
    return jsonify(list_directory(path))


@app.route("/api/state")
def api_state():
    """Aggregate all dashboard data."""
    return jsonify({
        "identity": {
            "name": "Vincent",
            "role": "AI Assistant for Grepples",
            "model": "Qwen3.6-27B-UD-Q4_K_XL.gguf",
            "provider": "custom (llama.cpp)",
            "runtime": "Hermes Agent",
            "connected_platforms": ["telegram", "local"],
        },
        "session": get_session_state(),
        "memory": get_memory_stats(),
        "sessions": get_session_stats(),
        "calibration": get_calibration_stats(),
        "system": get_system_health(),
        "gpu": get_gpu_stats(),
        "services": get_services(),
        "config": get_config(),
        "grounding": {
            "components": [
                {"name": "background-calibration", "status": "active"},
                {"name": "causal-models", "status": "active"},
                {"name": "counterfactual-testing", "status": "active"},
                {"name": "failure-mode-library", "status": "active"},
                {"name": "grounding-integration", "status": "active"},
                {"name": "multimodal-consistency", "status": "active"},
            ],
            "total": 6,
        },
        "cron_jobs": get_cron_jobs(),
        "skills": {
            "total": get_skills_count(),
        },
        "timestamp": datetime.now(timezone.utc).isoformat(),
    })


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5090, debug=False)