from __future__ import annotations

import json
import statistics
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from tools.orchestration_harness_v1 import summarize_plan


ROOT = Path(__file__).resolve().parents[2]
DEFAULT_HISTORY = ROOT / "Temp" / "pipeline_runtime_profile_history_v1.json"

CONTEXT_KEYS = (
    "harness_name",
    "mode",
    "validation_mode",
    "include_xlsx",
    "include_backups",
    "skip_validate",
)


def load_json(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {}
    try:
        obj = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return obj if isinstance(obj, dict) else {}


def load_history(path: Path) -> list[dict[str, Any]]:
    if not path.exists():
        return []
    try:
        obj = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return []
    return obj if isinstance(obj, list) else []


def _utc_now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _context_signature(profile: dict[str, Any]) -> str:
    context = profile.get("runtime_context") if isinstance(profile.get("runtime_context"), dict) else {}
    parts = []
    for key in CONTEXT_KEYS:
        value = profile.get(key)
        if value is None:
            value = context.get(key)
        parts.append(f"{key}={value}")
    return "|".join(parts)


def _safe_float(value: Any) -> float:
    try:
        return float(value)
    except Exception:
        return 0.0


def _safe_int(value: Any) -> int:
    try:
        return int(value)
    except Exception:
        return 0


def build_runtime_record(
    profile: dict[str, Any],
    analysis: dict[str, Any],
) -> dict[str, Any]:
    summary = profile.get("runtime_summary") if isinstance(profile.get("runtime_summary"), dict) else {}
    context = profile.get("runtime_context") if isinstance(profile.get("runtime_context"), dict) else {}
    return {
        "generated_at": profile.get("generated_at") or _utc_now(),
        "harness_name": profile.get("harness_name") or context.get("harness_name"),
        "context_signature": _context_signature(profile),
        "mode": profile.get("mode") or context.get("mode"),
        "validation_mode": profile.get("validation_mode") or context.get("validation_mode"),
        "include_xlsx": bool(profile.get("include_xlsx") if profile.get("include_xlsx") is not None else context.get("include_xlsx")),
        "include_backups": bool(profile.get("include_backups") if profile.get("include_backups") is not None else context.get("include_backups")),
        "skip_validate": bool(profile.get("skip_validate") if profile.get("skip_validate") is not None else context.get("skip_validate")),
        "step_count": _safe_int(summary.get("step_count")),
        "command_step_count": _safe_int(summary.get("command_step_count")),
        "callable_step_count": _safe_int(summary.get("callable_step_count")),
        "step_elapsed_sum_sec": _safe_float(summary.get("step_elapsed_sum_sec")),
        "critical_path_sec": _safe_float(summary.get("critical_path_sec")),
        "wall_clock_sec": _safe_float(summary.get("wall_clock_sec")),
        "parallel_width_max": _safe_int(summary.get("parallel_width_max")),
        "elapsed_sec_total": _safe_float(profile.get("elapsed_sec_total")),
        "file_count": _safe_int(profile.get("file_count")),
        "build_status": profile.get("build_status") or "OK",
        "anomaly_status": analysis.get("status") or "UNKNOWN",
        "anomaly_reason_codes": list(analysis.get("anomaly_reason_codes") or []),
        "baseline_window_size": _safe_int(analysis.get("baseline_window_size")),
        "baseline_elapsed_median_sec": _safe_float(analysis.get("baseline_elapsed_median_sec")),
        "baseline_critical_path_median_sec": _safe_float(analysis.get("baseline_critical_path_median_sec")),
        "delta_pct_vs_baseline": _safe_float(analysis.get("delta_pct_vs_baseline")),
        "critical_path_delta_pct": _safe_float(analysis.get("critical_path_delta_pct")),
        "runtime_shape_changed": bool(analysis.get("runtime_shape_changed")),
    }


def analyze_runtime_profile(
    profile: dict[str, Any],
    history: list[dict[str, Any]],
    *,
    min_samples: int = 5,
    max_delta_pct: float = 30.0,
    sigma_threshold: float = 2.0,
) -> dict[str, Any]:
    context_signature = _context_signature(profile)
    current_elapsed = _safe_float(profile.get("elapsed_sec_total"))
    current_critical = _safe_float((profile.get("runtime_summary") or {}).get("critical_path_sec"))
    current_step_count = _safe_int((profile.get("runtime_summary") or {}).get("step_count"))
    current_file_count = _safe_int(profile.get("file_count"))
    current_cache_hit = bool(profile.get("cache_hit"))

    comparable = [
        item for item in history
        if item.get("context_signature") == context_signature
        and item.get("build_status") == "OK"
        and item.get("anomaly_status") != "ALERT"
        and not bool(item.get("cache_hit"))
    ]
    if len(comparable) < min_samples:
        return {
            "status": "INSUFFICIENT_HISTORY",
            "anomaly_reason_codes": ["INSUFFICIENT_HISTORY"],
            "baseline_window_size": len(comparable),
            "baseline_elapsed_median_sec": 0.0,
            "baseline_critical_path_median_sec": 0.0,
            "baseline_elapsed_mean_sec": 0.0,
            "baseline_elapsed_stdev_sec": 0.0,
            "baseline_critical_path_mean_sec": 0.0,
            "baseline_critical_path_stdev_sec": 0.0,
            "delta_pct_vs_baseline": 0.0,
            "critical_path_delta_pct": 0.0,
            "runtime_shape_changed": False,
        }

    elapsed_values = [_safe_float(item.get("elapsed_sec_total")) for item in comparable]
    critical_values = [_safe_float(item.get("critical_path_sec")) for item in comparable]
    step_values = [_safe_int(item.get("step_count")) for item in comparable]
    file_values = [_safe_int(item.get("file_count")) for item in comparable]

    baseline_elapsed_median = statistics.median(elapsed_values)
    baseline_critical_median = statistics.median(critical_values)
    baseline_elapsed_mean = statistics.fmean(elapsed_values)
    baseline_critical_mean = statistics.fmean(critical_values)
    baseline_elapsed_stdev = statistics.pstdev(elapsed_values) if len(elapsed_values) > 1 else 0.0
    baseline_critical_stdev = statistics.pstdev(critical_values) if len(critical_values) > 1 else 0.0

    def _delta_pct(current: float, baseline: float) -> float:
        if baseline <= 0:
            return 0.0
        return ((current - baseline) / baseline) * 100.0

    elapsed_delta_pct = _delta_pct(current_elapsed, float(baseline_elapsed_median))
    critical_delta_pct = _delta_pct(current_critical, float(baseline_critical_median))
    elapsed_z = (current_elapsed - baseline_elapsed_mean) / baseline_elapsed_stdev if baseline_elapsed_stdev > 0 else 0.0
    critical_z = (current_critical - baseline_critical_mean) / baseline_critical_stdev if baseline_critical_stdev > 0 else 0.0
    runtime_shape_changed = (
        current_step_count != _safe_int(statistics.median(step_values))
        or current_file_count != _safe_int(statistics.median(file_values))
    )

    anomaly_reason_codes: list[str] = []
    if current_cache_hit:
        return {
            "status": "OK",
            "anomaly_reason_codes": [],
            "baseline_window_size": len(comparable),
            "baseline_elapsed_median_sec": round(float(baseline_elapsed_median), 3),
            "baseline_critical_path_median_sec": round(float(baseline_critical_median), 3),
            "baseline_elapsed_mean_sec": round(float(baseline_elapsed_mean), 3),
            "baseline_elapsed_stdev_sec": round(float(baseline_elapsed_stdev), 3),
            "baseline_critical_path_mean_sec": round(float(baseline_critical_mean), 3),
            "baseline_critical_path_stdev_sec": round(float(baseline_critical_stdev), 3),
            "delta_pct_vs_baseline": round(float(elapsed_delta_pct), 3),
            "critical_path_delta_pct": round(float(critical_delta_pct), 3),
            "elapsed_z_score": round(float(elapsed_z), 3),
            "critical_path_z_score": round(float(critical_z), 3),
            "runtime_shape_changed": runtime_shape_changed,
        }
    if abs(elapsed_delta_pct) >= max_delta_pct:
        anomaly_reason_codes.append("RUNTIME_SPEEDUP" if elapsed_delta_pct < 0 else "RUNTIME_SLOWDOWN")
    if abs(elapsed_z) >= sigma_threshold:
        anomaly_reason_codes.append("RUNTIME_ELAPSED_SIGMA_OUTLIER")
    if abs(critical_delta_pct) >= max_delta_pct:
        anomaly_reason_codes.append("CRITICAL_PATH_SPEEDUP" if critical_delta_pct < 0 else "CRITICAL_PATH_SLOWDOWN")
    if abs(critical_z) >= sigma_threshold:
        anomaly_reason_codes.append("CRITICAL_PATH_SIGMA_OUTLIER")

    if anomaly_reason_codes and not runtime_shape_changed:
        if elapsed_delta_pct < 0:
            anomaly_reason_codes.append("SAME_SHAPE_SPEEDUP")
        else:
            anomaly_reason_codes.append("SAME_SHAPE_SLOWDOWN")
    if elapsed_delta_pct < 0 and runtime_shape_changed:
        anomaly_reason_codes.append("SHAPE_CHANGE_SPEEDUP")
    if elapsed_delta_pct > 0 and runtime_shape_changed:
        anomaly_reason_codes.append("SHAPE_CHANGE_SLOWDOWN")

    status = "ALERT" if anomaly_reason_codes else "OK"
    return {
        "status": status,
        "anomaly_reason_codes": anomaly_reason_codes,
        "baseline_window_size": len(comparable),
        "baseline_elapsed_median_sec": round(float(baseline_elapsed_median), 3),
        "baseline_critical_path_median_sec": round(float(baseline_critical_median), 3),
        "baseline_elapsed_mean_sec": round(float(baseline_elapsed_mean), 3),
        "baseline_elapsed_stdev_sec": round(float(baseline_elapsed_stdev), 3),
        "baseline_critical_path_mean_sec": round(float(baseline_critical_mean), 3),
        "baseline_critical_path_stdev_sec": round(float(baseline_critical_stdev), 3),
        "delta_pct_vs_baseline": round(float(elapsed_delta_pct), 3),
        "critical_path_delta_pct": round(float(critical_delta_pct), 3),
        "elapsed_z_score": round(float(elapsed_z), 3),
        "critical_path_z_score": round(float(critical_z), 3),
        "runtime_shape_changed": runtime_shape_changed,
    }


def append_runtime_history(
    history_path: Path,
    record: dict[str, Any],
    *,
    max_entries: int = 20,
) -> list[dict[str, Any]]:
    history = load_history(history_path)
    history.append(record)
    if len(history) > max_entries:
        history = history[-max_entries:]
    history_path.parent.mkdir(parents=True, exist_ok=True)
    history_path.write_text(json.dumps(history, ensure_ascii=False, indent=2), encoding="utf-8")
    return history


def finalize_runtime_profile(
    *,
    profile_path: Path,
    history_path: Path = DEFAULT_HISTORY,
    payload: dict[str, Any],
    max_entries: int = 20,
    min_samples: int = 5,
    max_delta_pct: float = 30.0,
    sigma_threshold: float = 2.0,
) -> dict[str, Any]:
    history = load_history(history_path)
    analysis = analyze_runtime_profile(
        payload,
        history,
        min_samples=min_samples,
        max_delta_pct=max_delta_pct,
        sigma_threshold=sigma_threshold,
    )
    payload["runtime_anomaly"] = analysis
    payload["runtime_history_path"] = str(history_path)
    payload["runtime_history_size_before"] = len(history)
    profile_path.parent.mkdir(parents=True, exist_ok=True)
    profile_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    record = build_runtime_record(payload, analysis)
    append_runtime_history(history_path, record, max_entries=max_entries)
    payload["runtime_history_size_after"] = _safe_int(load_history(history_path).__len__())
    return analysis


def runtime_profile_from_steps(
    *,
    harness_name: str,
    mode: str,
    steps: list[dict[str, Any]],
    runtime_context: dict[str, Any] | None = None,
    file_count: int = 0,
    elapsed_sec_total: float | None = None,
    skipped_duplicate_steps: list[str] | None = None,
    gate_status: str | None = None,
) -> dict[str, Any]:
    summary = summarize_plan(steps)
    total_elapsed = float(elapsed_sec_total if elapsed_sec_total is not None else summary.get("step_elapsed_sum_sec") or 0.0)
    payload: dict[str, Any] = {
        "formula_id": "PIPELINE_RUNTIME_PROFILE_V1",
        "generated_at": _utc_now(),
        "harness_name": harness_name,
        "mode": mode,
        "elapsed_sec_total": round(total_elapsed, 3),
        "steps": steps,
        "runtime_summary": summary,
        "runtime_context": runtime_context or {},
        "file_count": int(file_count),
    }
    if skipped_duplicate_steps is not None:
        payload["skipped_duplicate_steps"] = skipped_duplicate_steps
        payload["duplicate_steps_removed_count"] = len(skipped_duplicate_steps)
    if gate_status is not None:
        payload["gate_status"] = gate_status
    return payload
