#!/usr/bin/env python3
"""
One-shot refresh job: run dashboard fetch scripts once and exit.

Snapshots are stored in MariaDB when configured, otherwise under data/*.json.
Schedule externally (Kubernetes CronJob, cron, or macOS LaunchAgent).

Tiers:
  live  — operational feeds (Jira IT Ops, Defender, Snipe alerts, Slack)
  daily — inventory / license reports (Google Workspace, Adobe, JetBrains, …)
  full  — both tiers (manual catch-up)

  ./scripts/run_dashboard_job.sh refresh-live
  ./scripts/run_dashboard_job.sh refresh-daily
  ./scripts/run_dashboard_job.sh refresh          # full

Production (Docker / Kubernetes):

  ./docker-entrypoint.sh refresh-live
  ./docker-entrypoint.sh refresh-daily
"""
from __future__ import annotations

import argparse
import os
import subprocess
import sys
import time
from datetime import datetime, timezone
from typing import Any

from paths import APP_ROOT, JOBS_DIR, load_env_file
from store import read_refresh_status, write_refresh_status as store_write_refresh_status

TIER_LIVE = "live"
TIER_DAILY = "daily"
TIER_FULL = "full"

# Operational data — schedule frequently (see env.example).
LIVE_FETCH_SCRIPTS_META: tuple[dict[str, str], ...] = (
    {
        "script": "fetch_m365_analytics.py",
        "json": "data/analytics.json",
        "label": "Microsoft 365",
    },
    {"script": "fetch_jira.py", "json": "data/jira.json", "label": "Jira"},
    {"script": "fetch_snipe_it.py", "json": "data/snipe-it.json", "label": "Snipe IT"},
    {
        "script": "fetch_slack_workspace.py",
        "json": "data/slack.json",
        "label": "Slack",
    },
)

# Heavy inventory / license reports — schedule once per day.
DAILY_FETCH_SCRIPTS_META: tuple[dict[str, str], ...] = (
    {
        "script": "fetch_google_workspace.py",
        "json": "data/google-workspace.json",
        "label": "Google Workspace",
    },
    {
        "script": "fetch_confluence.py",
        "json": "data/confluence.json",
        "label": "Confluence",
    },
    {
        "script": "fetch_atlassian_apps.py",
        "json": "data/atlassian-apps.json",
        "label": "Atlassian Apps",
    },
    {
        "script": "fetch_screaming_frog.py",
        "json": "data/screaming-frog.json",
        "label": "Screaming Frog",
    },
    {
        "script": "fetch_expressvpn.py",
        "json": "data/expressvpn.json",
        "label": "ExpressVPN",
    },
    {"script": "fetch_adobe.py", "json": "data/adobe.json", "label": "Adobe"},
    {"script": "fetch_jetbrains.py", "json": "data/jetbrains.json", "label": "JetBrains"},
)

FETCH_SCRIPTS_META: tuple[dict[str, str], ...] = (
    LIVE_FETCH_SCRIPTS_META + DAILY_FETCH_SCRIPTS_META
)

TIER_SCRIPTS: dict[str, tuple[dict[str, str], ...]] = {
    TIER_LIVE: LIVE_FETCH_SCRIPTS_META,
    TIER_DAILY: DAILY_FETCH_SCRIPTS_META,
    TIER_FULL: FETCH_SCRIPTS_META,
}

TIER_LABELS: dict[str, str] = {
    TIER_LIVE: "live (operational)",
    TIER_DAILY: "daily (reports)",
    TIER_FULL: "full",
}

_PROXY_ENV_KEYS = (
    "HTTP_PROXY",
    "HTTPS_PROXY",
    "ALL_PROXY",
    "http_proxy",
    "https_proxy",
    "all_proxy",
    "SOCKS_PROXY",
    "SOCKS5_PROXY",
    "socks_proxy",
    "socks5_proxy",
    "GIT_HTTP_PROXY",
    "GIT_HTTPS_PROXY",
)


def subprocess_env_no_proxy() -> dict[str, str]:
    env = dict(os.environ)
    for key in _PROXY_ENV_KEYS:
        env.pop(key, None)
    return env


def utc_now_iso() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def write_refresh_status(payload: dict[str, Any]) -> None:
    payload["updatedAt"] = utc_now_iso()
    store_write_refresh_status(payload)


def _load_refresh_status_data() -> dict[str, Any] | None:
    data = read_refresh_status()
    return data if isinstance(data, dict) else None


def load_previous_durations() -> dict[str, float]:
    try:
        data = _load_refresh_status_data() or {}
    except Exception:
        return {}
    out: dict[str, float] = {}
    for row in data.get("scripts") or []:
        if not isinstance(row, dict):
            continue
        script = str(row.get("script") or "").strip()
        dur = row.get("lastDurationSec")
        if script and isinstance(dur, (int, float)) and not isinstance(dur, bool):
            out[script] = float(dur)
    return out


def _blank_script_row(meta: dict[str, str], *, last_dur: float | None = None) -> dict[str, Any]:
    return {
        "script": meta["script"],
        "json": meta["json"],
        "label": meta["label"],
        "status": "pending",
        "startedAt": None,
        "finishedAt": None,
        "durationSec": None,
        "lastDurationSec": last_dur,
        "exitCode": None,
    }


def _tier_script_names(tier: str) -> frozenset[str]:
    return frozenset(m["script"] for m in TIER_SCRIPTS[tier])


def merge_status_scripts(status: dict[str, Any]) -> dict[str, Any]:
    prev_durations = load_previous_durations()
    existing: dict[str, dict[str, Any]] = {}
    for row in status.get("scripts") or []:
        if not isinstance(row, dict):
            continue
        script = str(row.get("script") or "").strip()
        if script:
            existing[script] = row
    merged: list[dict[str, Any]] = []
    for meta in FETCH_SCRIPTS_META:
        script = meta["script"]
        if script in existing:
            row = dict(existing[script])
            row["json"] = meta["json"]
            row["label"] = meta["label"]
            merged.append(row)
        else:
            merged.append(_blank_script_row(meta, last_dur=prev_durations.get(script)))
    status["scripts"] = merged
    return status


def new_cycle_status(*, tier: str, phase: str = "running") -> dict[str, Any]:
    prev_durations = load_previous_durations()
    prev = _load_refresh_status_data() or {}
    prev_by_script: dict[str, dict[str, Any]] = {}
    for row in prev.get("scripts") or []:
        if isinstance(row, dict):
            script = str(row.get("script") or "").strip()
            if script:
                prev_by_script[script] = row

    run_names = _tier_script_names(tier)
    scripts: list[dict[str, Any]] = []
    for meta in FETCH_SCRIPTS_META:
        script = meta["script"]
        if script in run_names:
            scripts.append(_blank_script_row(meta, last_dur=prev_durations.get(script)))
        elif script in prev_by_script:
            row = dict(prev_by_script[script])
            row["json"] = meta["json"]
            row["label"] = meta["label"]
            scripts.append(row)
        else:
            scripts.append(_blank_script_row(meta, last_dur=prev_durations.get(script)))

    return {
        "phase": phase,
        "jobTier": tier,
        "cycleStartedAt": utc_now_iso(),
        "cycleFinishedAt": None,
        "currentScript": None,
        "scripts": scripts,
    }


def script_index_map() -> dict[str, int]:
    return {meta["script"]: idx for idx, meta in enumerate(FETCH_SCRIPTS_META)}


def run_script(name: str) -> int:
    path = JOBS_DIR / name
    print(f"\n--- {name} ---", flush=True)
    r = subprocess.run(
        [sys.executable, str(path)],
        cwd=str(APP_ROOT),
        check=False,
        env=subprocess_env_no_proxy(),
    )
    code = int(r.returncode)
    if code == 0:
        print(f"[ok] {name}", flush=True)
    else:
        print(f"[fail] {name} exited with code {code}", flush=True)
    return code


def _print_refresh_summary(tier: str, results: list[tuple[str, int]]) -> None:
    ok = [name for name, code in results if code == 0]
    failed = [(name, code) for name, code in results if code != 0]
    print(f"\n=== Refresh summary ({TIER_LABELS[tier]}) ===", flush=True)
    for name in ok:
        print(f"  OK   {name}", flush=True)
    for name, code in failed:
        print(f"  FAIL {name} (exit {code})", flush=True)
    print(
        f"{len(ok)} succeeded, {len(failed)} failed — job completed",
        flush=True,
    )


def run_refresh_job(tier: str) -> int:
    scripts_meta = TIER_SCRIPTS[tier]
    indices = script_index_map()
    status = new_cycle_status(tier=tier)
    write_refresh_status(status)
    results: list[tuple[str, int]] = []

    for meta in scripts_meta:
        name = meta["script"]
        idx = indices[name]
        row = status["scripts"][idx]
        row["status"] = "running"
        row["startedAt"] = utc_now_iso()
        status["currentScript"] = name
        write_refresh_status(status)

        started = time.time()
        code = run_script(name)
        duration = max(time.time() - started, 0.0)
        results.append((name, code))

        row["status"] = "failed" if code != 0 else "done"
        row["finishedAt"] = utc_now_iso()
        row["durationSec"] = round(duration, 1)
        row["lastDurationSec"] = round(duration, 1)
        row["exitCode"] = code
        status["currentScript"] = None
        write_refresh_status(status)

    failed_count = sum(1 for _, code in results if code != 0)
    status["phase"] = "idle"
    status["cycleFinishedAt"] = utc_now_iso()
    status["lastRunSuccessCount"] = len(results) - failed_count
    status["lastRunFailedCount"] = failed_count
    write_refresh_status(status)
    _print_refresh_summary(tier, results)
    # Individual fetch failures are recorded in refresh-status; exit 0 so K8s jobs succeed.
    return 0


def main() -> None:
    p = argparse.ArgumentParser(
        description="Run dashboard fetch scripts once (schedule live and daily tiers externally)."
    )
    p.add_argument(
        "--tier",
        choices=(TIER_LIVE, TIER_DAILY, TIER_FULL),
        default=TIER_FULL,
        help="live=operational feeds; daily=inventory reports; full=both (default)",
    )
    p.add_argument("--once", action="store_true", help=argparse.SUPPRESS)
    p.add_argument("--interval", type=int, default=None, metavar="SEC", help=argparse.SUPPRESS)
    args = p.parse_args()

    if args.interval is not None:
        print(
            "[warn] --interval is deprecated; refresh is a one-shot job. "
            "Schedule live and daily tiers with Kubernetes CronJob, cron, or LaunchAgent.",
            file=sys.stderr,
        )

    load_env_file()
    try:
        current = _load_refresh_status_data()
        if isinstance(current, dict):
            write_refresh_status(merge_status_scripts(current))
    except Exception:
        pass

    tier = args.tier
    scripts_meta = TIER_SCRIPTS[tier]
    print(
        f"Running {TIER_LABELS[tier]} refresh ({len(scripts_meta)} script(s)): "
        + ", ".join(m["script"] for m in scripts_meta),
        flush=True,
    )
    raise SystemExit(run_refresh_job(tier))


if __name__ == "__main__":
    main()
