#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import os
import re
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
from urllib import error as urlerror
from urllib import parse as urlparse
from urllib import request as urlrequest

TOKYO = timezone(timedelta(hours=9))
UTC = timezone.utc


def now_tokyo() -> datetime:
    return datetime.now(TOKYO)


def iso_from_ms(value: Any) -> Optional[str]:
    if value is None:
        return None
    try:
        ms = int(value)
    except (TypeError, ValueError):
        return None
    return datetime.fromtimestamp(ms / 1000, tz=TOKYO).isoformat()


def parse_every_to_seconds(raw: Any) -> Optional[int]:
    if raw is None:
        return None
    text = str(raw).strip().lower()
    if text in {"", "disabled", "off", "false", "none", "0"}:
        return None
    match = re.match(r"^(\d+)\s*([smhd])$", text)
    if not match:
        return None
    n = int(match.group(1))
    unit = match.group(2)
    return {
        "s": n,
        "m": n * 60,
        "h": n * 3600,
        "d": n * 86400,
    }[unit]


def read_json(path: Path, default: Any) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return default


def write_json(path: Path, payload: Dict[str, Any]) -> None:
    tmp = path.with_suffix(".json.tmp")
    tmp.write_text(
        json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    tmp.replace(path)


def to_float(value: Any) -> Optional[float]:
    if isinstance(value, (int, float)):
        return float(value)
    if isinstance(value, str):
        try:
            return float(value.strip())
        except ValueError:
            return None
    return None


def status_from_last_ms(last_ms: Optional[int], active_minutes: int = 90) -> str:
    if not last_ms:
        return "pending"
    age_minutes = (now_tokyo().timestamp() * 1000 - last_ms) / 60000
    return "online" if age_minutes <= active_minutes else "pending"


def load_last_runs(runs_dir: Path) -> Dict[str, Dict[str, Any]]:
    by_job: Dict[str, Dict[str, Any]] = {}
    if not runs_dir.exists():
        return by_job
    for file in runs_dir.glob("*.jsonl"):
        try:
            lines = [ln for ln in file.read_text(encoding="utf-8").splitlines() if ln.strip()]
        except Exception:
            continue
        if not lines:
            continue
        try:
            event = json.loads(lines[-1])
        except Exception:
            continue
        if not isinstance(event, dict):
            continue
        job_id = event.get("jobId") or file.stem
        prev = by_job.get(job_id)
        prev_ts = int(prev.get("runAtMs") or 0) if prev else 0
        cur_ts = int(event.get("runAtMs") or 0)
        if cur_ts >= prev_ts:
            by_job[job_id] = event
    return by_job


def collect_sessions(session_file: Path) -> List[Dict[str, Any]]:
    data = read_json(session_file, {})
    if not isinstance(data, dict):
        return []
    rows: List[Dict[str, Any]] = []
    for key, item in data.items():
        if not isinstance(item, dict):
            continue
        rows.append(
            {
                "key": key,
                "updatedAt": int(item.get("updatedAt") or 0),
                "model": item.get("model") or item.get("modelId") or "unknown",
                "provider": item.get("modelProvider") or item.get("provider"),
                "sessionId": item.get("sessionId"),
                "inputTokens": item.get("inputTokens"),
                "outputTokens": item.get("outputTokens"),
                "totalTokens": item.get("totalTokens"),
            }
        )
    rows.sort(key=lambda x: x.get("updatedAt", 0), reverse=True)
    return rows


def find_latest_ms(sessions: List[Dict[str, Any]], matcher) -> Optional[int]:
    for s in sessions:
        if matcher(s):
            return s.get("updatedAt")
    return None


def count_sessions(sessions: List[Dict[str, Any]], matcher) -> int:
    return sum(1 for s in sessions if matcher(s))


def sum_cost_items(items: Any) -> float:
    if not isinstance(items, list):
        return 0.0
    total = 0.0
    for it in items:
        if not isinstance(it, dict):
            continue
        for key in ("cost_usd", "amount", "value", "cost", "usd"):
            if key in it:
                val = to_float(it.get(key))
                if val is not None:
                    total += val
    return total


def fetch_monthly_anthropic_cost_usd(admin_key: str) -> Dict[str, Any]:
    now = datetime.now(UTC)
    start = datetime(now.year, now.month, 1, tzinfo=UTC)
    params = urlparse.urlencode(
        {
            "starting_at": start.strftime("%Y-%m-%dT%H:%M:%SZ"),
            "ending_at": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
            "bucket_width": "1d",
        }
    )
    url = f"https://api.anthropic.com/v1/organizations/cost_report?{params}"
    req = urlrequest.Request(
        url,
        headers={
            "x-api-key": admin_key,
            "anthropic-version": "2023-06-01",
        },
    )
    try:
        with urlrequest.urlopen(req, timeout=8) as resp:
            payload = json.loads(resp.read().decode("utf-8", "ignore"))
            data = payload.get("data")
            total = sum_cost_items(data)
            return {
                "ok": True,
                "status": int(resp.getcode()),
                "usedUsdMonth": round(total, 4),
            }
    except urlerror.HTTPError as e:
        body = e.read().decode("utf-8", "ignore")
        detail = ""
        try:
            detail = json.loads(body).get("error", {}).get("message", "")
        except Exception:
            detail = body[:120]
        return {"ok": False, "status": e.code, "error": detail or "http error"}
    except Exception as e:
        return {"ok": False, "status": None, "error": f"{type(e).__name__}: {e}"}


def build_haiku_credits(status_dir: Path) -> Dict[str, Any]:
    credit_cfg = read_json(status_dir / "credit-config.json", {})
    haiku_cfg = credit_cfg.get("haiku", {}) if isinstance(credit_cfg, dict) else {}

    low_threshold = to_float(haiku_cfg.get("lowThresholdUsd"))
    critical_threshold = to_float(haiku_cfg.get("criticalThresholdUsd"))
    monthly_budget = to_float(haiku_cfg.get("monthlyBudgetUsd"))
    manual_remaining = to_float(haiku_cfg.get("manualRemainingUsd"))
    low_threshold = 20.0 if low_threshold is None else low_threshold
    critical_threshold = 5.0 if critical_threshold is None else critical_threshold

    remaining: Optional[float] = None
    used: Optional[float] = None
    source = "unavailable"
    note = ""
    available = False

    if manual_remaining is not None:
        remaining = manual_remaining
        source = "manual"
        available = True
        note = "credit-config.json の manualRemainingUsd から同期"
        if monthly_budget is not None:
            used = max(0.0, monthly_budget - remaining)
    else:
        admin_key = os.getenv("ANTHROPIC_ADMIN_API_KEY", "").strip()
        if admin_key.startswith("sk-ant-admin"):
            report = fetch_monthly_anthropic_cost_usd(admin_key)
            if report.get("ok"):
                used = to_float(report.get("usedUsdMonth"))
                source = "admin-api"
                if monthly_budget is not None and used is not None:
                    remaining = max(0.0, monthly_budget - used)
                    available = True
                    note = "Anthropic Admin API の月次コストから算出"
                else:
                    note = "月次コストは取得できたが monthlyBudgetUsd 未設定のため残高算出不可"
            else:
                source = "admin-api"
                note = f"Admin API取得失敗: {report.get('error', 'unknown')}"
        else:
            source = "standard-key"
            note = "残クレジット同期には ANTHROPIC_ADMIN_API_KEY（sk-ant-admin...）が必要"

    state = "unknown"
    if remaining is not None:
        if remaining <= critical_threshold:
            state = "critical"
        elif remaining <= low_threshold:
            state = "low"
        else:
            state = "ok"

    help_required = state in {"low", "critical"}
    help_message = ""
    if state == "critical":
        help_message = "Haiku: クレジットが危険域。補充をお願いします。"
    elif state == "low":
        help_message = "Haiku: クレジットが少ないです。補充サポートお願いします。"

    return {
        "provider": "anthropic",
        "model": "claude-haiku-4-5-20251001",
        "state": state,
        "source": source,
        "available": available,
        "remainingUsd": None if remaining is None else round(remaining, 4),
        "usedUsdMonth": None if used is None else round(used, 4),
        "monthlyBudgetUsd": monthly_budget,
        "lowThresholdUsd": low_threshold,
        "criticalThresholdUsd": critical_threshold,
        "helpRequired": help_required,
        "helpMessage": help_message,
        "note": note,
        "lastCheckedAt": now_tokyo().isoformat(),
    }


def build_agents(
    config: Dict[str, Any],
    sessions: List[Dict[str, Any]],
    haiku_credits: Dict[str, Any],
) -> Dict[str, Dict[str, Any]]:
    defaults = (((config.get("agents") or {}).get("defaults") or {}))
    model_cfg = defaults.get("model") or {}
    primary = model_cfg.get("primary") or "anthropic/claude-haiku-4-5-20251001"
    fallbacks = list(model_cfg.get("fallbacks") or [])

    agents: Dict[str, Dict[str, Any]] = {}

    brain_last = find_latest_ms(
        sessions,
        lambda s: "claude-haiku" in str(s.get("model", "")).lower()
        or s.get("key") == "agent:main:main",
    )
    agents["brain"] = {
        "name": "Brain",
        "nickname": "Benkei",
        "model": primary,
        "role": "strategy/routing/QC",
        "taskFocus": "ミッション方針決定・ルーティング・最終品質チェック",
        "status": status_from_last_ms(brain_last, active_minutes=180),
        "tasksCompleted": count_sessions(sessions, lambda s: ":run:" in s.get("key", "")),
        "lastActivity": iso_from_ms(brain_last),
        "haikuCredits": haiku_credits,
        "color": "#4CAF50",
    }

    if any("qwen3.5:35b-a3b" in f for f in fallbacks):
        last = find_latest_ms(sessions, lambda s: "qwen3.5:35b-a3b" in str(s.get("model", "")))
        agents["muscle_qwen35"] = {
            "name": "Worker A (qwen3.5)",
            "model": "ollama/qwen3.5:35b-a3b",
            "role": "research/bulk-ops",
            "taskFocus": "リサーチ・情報統合・大量処理オペレーション",
            "status": status_from_last_ms(last),
            "tasksCompleted": count_sessions(sessions, lambda s: "qwen3.5:35b-a3b" in str(s.get("model", ""))),
            "lastActivity": iso_from_ms(last),
            "color": "#FF9800",
        }

    if any("qwen2.5-coder" in f for f in fallbacks):
        last = find_latest_ms(sessions, lambda s: "qwen2.5-coder" in str(s.get("model", "")))
        agents["muscle_qwen_coder"] = {
            "name": "Worker B (qwen-coder)",
            "model": "ollama/qwen2.5-coder:32b",
            "role": "coding/build-tasks",
            "taskFocus": "実装・テスト・バグ修正・自動化スクリプト作成",
            "status": status_from_last_ms(last),
            "tasksCompleted": count_sessions(sessions, lambda s: "qwen2.5-coder" in str(s.get("model", ""))),
            "lastActivity": iso_from_ms(last),
            "color": "#2196F3",
        }

    if any("qwen3.5:9b" in f for f in fallbacks):
        last = find_latest_ms(sessions, lambda s: "qwen3.5:9b" in str(s.get("model", "")))
        agents["muscle_qwen9"] = {
            "name": "Worker D (qwen3.5-lite)",
            "model": "ollama/qwen3.5:9b",
            "role": "heartbeat/triage/light-ops",
            "taskFocus": "Heartbeat監視・ログ一次分類・軽量タスク処理",
            "status": status_from_last_ms(last),
            "tasksCompleted": count_sessions(sessions, lambda s: "qwen3.5:9b" in str(s.get("model", ""))),
            "lastActivity": iso_from_ms(last),
            "color": "#00BCD4",
        }

    return agents


def build_cron_jobs(
    config: Dict[str, Any],
    cron_jobs_file: Path,
    runs_by_job: Dict[str, Dict[str, Any]],
) -> Dict[str, Dict[str, Any]]:
    defaults = (((config.get("agents") or {}).get("defaults") or {}))
    heartbeat = defaults.get("heartbeat") or {}
    heartbeat_every = heartbeat.get("every")
    heartbeat_seconds = parse_every_to_seconds(heartbeat_every)
    heartbeat_enabled = heartbeat_seconds is not None

    now = now_tokyo()
    heartbeat_last = None
    computed_next_dt = (now + timedelta(seconds=heartbeat_seconds)) if heartbeat_enabled else None
    heartbeat_next = computed_next_dt.isoformat() if computed_next_dt else None
    heartbeat_model = heartbeat.get("model") or "ollama/qwen3.5:35b-a3b"

    for run in runs_by_job.values():
        summary = str(run.get("summary") or "").upper()
        if "HEARTBEAT" in summary:
            heartbeat_last = iso_from_ms(run.get("runAtMs"))
            candidate_next = iso_from_ms(run.get("nextRunAtMs"))
            if candidate_next:
                candidate_dt = datetime.fromisoformat(candidate_next)
                if computed_next_dt and candidate_dt > computed_next_dt:
                    heartbeat_next = candidate_next
            heartbeat_model = run.get("provider") and run.get("model") and f"{run.get('provider')}/{run.get('model')}" or heartbeat_model
            break

    jobs = {
        "heartbeat": {
            "name": "Heartbeat Poll",
            "schedule": f"Every {heartbeat_every}" if heartbeat_enabled else "disabled",
            "status": "active" if heartbeat_enabled else "inactive",
            "lastRun": heartbeat_last,
            "nextRun": heartbeat_next,
            "model": heartbeat_model,
            "color": "#4CAF50" if heartbeat_enabled else "#F44336",
        }
    }

    cron_data = read_json(cron_jobs_file, {})
    for job in cron_data.get("jobs", []) if isinstance(cron_data, dict) else []:
        if not isinstance(job, dict):
            continue
        job_id = str(job.get("id") or job.get("name") or "job").strip()
        if not job_id:
            continue
        run = runs_by_job.get(job_id, {})
        enabled = bool(job.get("enabled", True))
        jobs[job_id] = {
            "name": job.get("name") or job_id,
            "schedule": job.get("schedule") or job.get("every") or "custom",
            "status": "active" if enabled else "inactive",
            "lastRun": iso_from_ms(run.get("runAtMs") or job.get("lastRunAtMs")),
            "nextRun": iso_from_ms(run.get("nextRunAtMs") or job.get("nextRunAtMs")),
            "model": run.get("provider") and run.get("model") and f"{run.get('provider')}/{run.get('model')}" or None,
            "color": "#4CAF50" if enabled else "#F44336",
        }
    return jobs


def build_live_office(
    sessions: List[Dict[str, Any]],
    cron_jobs: Dict[str, Dict[str, Any]],
    runs_by_job: Dict[str, Dict[str, Any]],
    haiku_credits: Dict[str, Any],
) -> Dict[str, Any]:
    now_ms = int(now_tokyo().timestamp() * 1000)
    pulse = []
    for s in sessions[:24]:
        updated = int(s.get("updatedAt") or 0)
        age_sec = max(0, (now_ms - updated) // 1000) if updated else None
        pulse.append(
            {
                "key": s.get("key"),
                "model": s.get("model"),
                "provider": s.get("provider"),
                "updatedAt": iso_from_ms(updated),
                "ageSec": age_sec,
                "inputTokens": s.get("inputTokens"),
                "outputTokens": s.get("outputTokens"),
            }
        )

    runs = []
    for job_id, run in sorted(
        runs_by_job.items(),
        key=lambda kv: int((kv[1] or {}).get("runAtMs") or 0),
        reverse=True,
    )[:12]:
        runs.append(
            {
                "jobId": job_id,
                "status": run.get("status"),
                "summary": run.get("summary"),
                "model": run.get("model"),
                "provider": run.get("provider"),
                "runAt": iso_from_ms(run.get("runAtMs")),
                "durationMs": run.get("durationMs"),
            }
        )

    heartbeat = cron_jobs.get("heartbeat", {})
    return {
        "updatedAt": now_tokyo().isoformat(),
        "wallClock": now_tokyo().strftime("%Y-%m-%d %H:%M:%S %Z"),
        "heartbeat": {
            "enabled": heartbeat.get("status") == "active",
            "every": heartbeat.get("schedule"),
            "model": heartbeat.get("model"),
            "lastRun": heartbeat.get("lastRun"),
            "nextRun": heartbeat.get("nextRun"),
        },
        "haikuCredits": haiku_credits,
        "sessionPulse": pulse,
        "recentRuns": runs,
    }


def update_status(status_path: Path, state_dir: Path) -> None:
    openclaw_path = state_dir / "openclaw.json"
    session_file = state_dir / "agents" / "main" / "sessions" / "sessions.json"
    cron_jobs_file = state_dir / "cron" / "jobs.json"
    runs_dir = state_dir / "cron" / "runs"
    status_dir = status_path.parent

    base = read_json(status_path, {})
    if not isinstance(base, dict):
        base = {}
    config = read_json(openclaw_path, {})
    sessions = collect_sessions(session_file)
    runs_by_job = load_last_runs(runs_dir)

    base.setdefault(
        "kpi",
        {"recovery": {"targetYen": 370000, "currentYen": 0, "percentageComplete": 0, "status": "in-progress", "monthlyGoal": 62000}},
    )
    base.setdefault("taskQueue", {"pending": [], "active": [], "completed": []})
    base.setdefault(
        "missionStatement",
        "37万円を回収しながら、AIエージェント組織が24/7で価値を生むCactus Empireを構築する。",
    )

    haiku_credits = build_haiku_credits(status_dir)
    agents = build_agents(config, sessions, haiku_credits)
    cron_jobs = build_cron_jobs(config, cron_jobs_file, runs_by_job)
    live_office = build_live_office(sessions, cron_jobs, runs_by_job, haiku_credits)

    base["meta"] = {
        **(base.get("meta") or {}),
        "lastUpdated": now_tokyo().isoformat(),
        "version": "1.2.0",
        "source": "mission-control/sync_status.py",
        "refreshInstructions": "sync_status.py で自動同期（手動編集可）",
    }
    base["agents"] = agents
    base["cronJobs"] = cron_jobs
    base["credits"] = {"haiku": haiku_credits}
    base["liveOffice"] = live_office

    write_json(status_path, base)


def main() -> None:
    parser = argparse.ArgumentParser(description="Sync Mission Control status.json with live Openclaw state")
    parser.add_argument("--watch", action="store_true", help="Run continuously")
    parser.add_argument("--interval", type=int, default=20, help="Watch interval seconds")
    args = parser.parse_args()

    status_path = Path(__file__).resolve().parent / "status.json"
    state_dir = Path(__file__).resolve().parents[2]

    if args.watch:
        while True:
            update_status(status_path, state_dir)
            time.sleep(max(5, args.interval))
    else:
        update_status(status_path, state_dir)


if __name__ == "__main__":
    main()
