from __future__ import annotations

import argparse
import json
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
from zoneinfo import ZoneInfo


ROOT = Path(__file__).resolve().parents[1]
WORKSPACE = ROOT.parent
DEFAULT_DATA_DIR = WORKSPACE / "medical-tools-extension-data" / "consultations"
DEFAULT_TIMEZONE = "Europe/Kiev"
PRIVACY_LINE = "privacy: no audio, no transcript text, no Lakmus field text, no license key"


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Aggregate Parallax Health workflow metrics without reading clinical text."
    )
    parser.add_argument("--data-dir", default=str(DEFAULT_DATA_DIR), help="Consultations data directory.")
    parser.add_argument("--limit", type=int, default=20, help="Number of recent sessions to inspect.")
    parser.add_argument("--source", default="", help="Optional source filter: microphone, file_test, readiness.")
    parser.add_argument("--timezone", default=DEFAULT_TIMEZONE, help="Timezone for local times.")
    parser.add_argument("--stale-minutes", type=int, default=30, help="Mark unfinished sessions stale after this many minutes without updates.")
    parser.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
    args = parser.parse_args()

    data_dir = Path(args.data_dir).expanduser()
    report = build_report(data_dir, max(1, args.limit), args.source, args.timezone, max(1, args.stale_minutes))
    if args.json:
        print(json.dumps(report, ensure_ascii=False, indent=2))
    else:
        print(format_report(report))
    return 0


def build_report(data_dir: Path, limit: int, source_filter: str, timezone_name: str, stale_minutes: int) -> dict:
    rows = build_rows(data_dir, limit, source_filter, timezone_name, stale_minutes)
    return {
        "title": "Parallax Health workflow metrics report",
        "privacy": PRIVACY_LINE.removeprefix("privacy: "),
        "checkedAt": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
        "timezone": timezone_name,
        "sourceFilter": normalize_source_filter(source_filter) or "any",
        "staleMinutes": stale_minutes,
        "limit": limit,
        "summary": aggregate_rows(rows),
        "actionGuide": action_guide(),
        "rows": rows,
    }


def build_rows(data_dir: Path, limit: int, source_filter: str, timezone_name: str, stale_minutes: int) -> list[dict]:
    if not data_dir.is_dir():
        return []
    tz = ZoneInfo(timezone_name)
    now_utc = datetime.now(timezone.utc)
    normalized_source = normalize_source_filter(source_filter)
    session_dirs = [
        path
        for path in data_dir.iterdir()
        if path.is_dir() and source_matches(read_safe_session_source(path), normalized_source)
    ]
    session_dirs.sort(key=session_metric_mtime, reverse=True)
    return [build_row(session_dir, tz, now_utc, stale_minutes) for session_dir in session_dirs[:limit]]


def build_row(session_dir: Path, tz: ZoneInfo, now_utc: datetime, stale_minutes: int) -> dict:
    session = load_json(session_dir / "session.json")
    status = load_json(session_dir / "status.json")
    metrics = load_json(session_dir / "metrics.json")
    started_at = first_text(status.get("startedAt"), metrics.get("startedAt"), session.get("startedAt"))
    finished_at = first_text(status.get("finishedAt"), metrics.get("finishedAt"), session.get("finishedAt"))
    status_text = first_text(status.get("status"), metrics.get("status"), session.get("status"))
    processing_state = first_text(status.get("processingState"), metrics.get("processingState"), session.get("processingState"))
    last_updated_at = datetime.fromtimestamp(session_metric_mtime(session_dir), timezone.utc)
    age_minutes = max(0, (now_utc - last_updated_at).total_seconds() / 60)
    activity_state = classify_activity_state(status_text, processing_state, finished_at, age_minutes, stale_minutes)
    duration_seconds = first_number(metrics.get("durationSeconds"), status.get("durationSeconds"))
    if duration_seconds is None:
        duration_seconds = seconds_between(started_at, finished_at)
    source = first_text(
        status.get("sessionSource"),
        metrics.get("sessionSource"),
        status.get("source"),
        metrics.get("source"),
        session.get("source"),
        "microphone",
    )
    chunk_count = int_number(first_number(status.get("chunkCount"), metrics.get("chunkCount")))
    done_chunk_count = int_number(first_number(status.get("doneChunkCount"), metrics.get("doneChunkCount")))
    error_chunk_count = int_number(first_number(status.get("errorCount"), metrics.get("errorChunkCount")))
    fields_with_text = int_number(first_number(metrics.get("draftFieldsWithText"), status.get("draftFieldsWithText")))
    manual_minutes = first_number(metrics.get("estimatedManualTypingMinutes"))
    review_minutes = first_number(metrics.get("estimatedReviewMinutes"))
    saved_minutes = first_number(metrics.get("estimatedNetSavedMinutes"))
    coverage_percent = first_number(metrics.get("automationCoveragePercent"))
    latency_seconds = first_number(metrics.get("firstTranscriptLatencySeconds"))
    active_ratio = first_number(metrics.get("avgActiveFrameRatio"))
    rms_db = first_number(metrics.get("avgInputRmsDb"))
    recommended_action = recommended_action_for(activity_state, error_chunk_count, fields_with_text)
    return {
        "sessionId": session_dir.name,
        "source": compact_source_label(source),
        "status": status_text,
        "processingState": processing_state,
        "activityState": activity_state,
        "startedLocal": local_time(started_at, tz),
        "finishedLocal": local_time(finished_at, tz),
        "lastUpdatedLocal": last_updated_at.astimezone(tz).strftime("%Y-%m-%d %H:%M"),
        "ageMinutes": round_number(age_minutes, 1),
        "durationSeconds": round_number(duration_seconds, 0),
        "audioMinutes": round_number((duration_seconds or 0) / 60, 2),
        "chunkCount": chunk_count,
        "doneChunkCount": done_chunk_count,
        "errorChunkCount": error_chunk_count,
        "segmentCount": int_number(first_number(metrics.get("segmentCount"), status.get("segmentCount"))),
        "draftFieldsWithText": fields_with_text,
        "manualTypingMinutes": round_number(manual_minutes, 2),
        "reviewMinutes": round_number(review_minutes, 2),
        "netSavedMinutes": round_number(saved_minutes, 2),
        "automationCoveragePercent": round_number(coverage_percent, 1),
        "firstTranscriptLatencySeconds": round_number(latency_seconds, 1),
        "avgInputRmsDb": round_number(rms_db, 1),
        "avgActiveFrameRatio": round_number(active_ratio, 3),
        "technicalErrors": error_chunk_count,
        "recommendedAction": recommended_action,
        "actionRequired": action_required(recommended_action),
    }


def aggregate_rows(rows: list[dict]) -> dict:
    numeric_keys = [
        "audioMinutes",
        "manualTypingMinutes",
        "reviewMinutes",
        "netSavedMinutes",
        "automationCoveragePercent",
        "firstTranscriptLatencySeconds",
        "avgInputRmsDb",
        "avgActiveFrameRatio",
    ]
    source_counts = Counter(row["source"] for row in rows)
    activity_counts = Counter(row["activityState"] for row in rows)
    finished_rows = [row for row in rows if str(row.get("status") or "").lower() == "finished"]
    unfinished_rows = [row for row in rows if row.get("activityState") != "finished"]
    stale_rows = [row for row in rows if row.get("activityState") == "stale_recording"]
    active_rows = [row for row in rows if row.get("activityState") == "active_recording"]
    rows_with_fields = [row for row in rows if int(row.get("draftFieldsWithText") or 0) > 0]
    action_counts = Counter(row["recommendedAction"] for row in rows)
    summary = {
        "sessionCount": len(rows),
        "finishedSessionCount": len(finished_rows),
        "unfinishedSessionCount": len(unfinished_rows),
        "activeSessionCount": len(active_rows),
        "staleSessionCount": len(stale_rows),
        "actionRequiredCount": sum(1 for row in rows if row.get("actionRequired")),
        "sessionsWithFieldText": len(rows_with_fields),
        "sourceDistribution": dict(sorted(source_counts.items())),
        "activityDistribution": dict(sorted(activity_counts.items())),
        "actionDistribution": dict(sorted(action_counts.items())),
        "totalAudioMinutes": round_number(sum_values(rows, "audioMinutes"), 2),
        "totalManualTypingMinutes": round_number(sum_values(rows, "manualTypingMinutes"), 2),
        "totalReviewMinutes": round_number(sum_values(rows, "reviewMinutes"), 2),
        "totalNetSavedMinutes": round_number(sum_values(rows, "netSavedMinutes"), 2),
        "totalChunks": int(sum_values(rows, "chunkCount")),
        "totalTechnicalErrors": int(sum_values(rows, "technicalErrors")),
        "averageFieldsWithText": round_number(average(rows, "draftFieldsWithText"), 2),
    }
    for key in numeric_keys:
        summary[f"average{key[0].upper()}{key[1:]}"] = round_number(average(rows, key), 2)
    manual_total = summary["totalManualTypingMinutes"] or 0
    saved_total = summary["totalNetSavedMinutes"] or 0
    summary["estimatedReductionPercent"] = round_number((saved_total / manual_total) * 100, 1) if manual_total > 0 else 0
    return summary


def format_report(report: dict) -> str:
    summary = report["summary"]
    lines = [
        report["title"],
        f"privacy: {report['privacy']}",
        f"checkedAt: {report['checkedAt']}",
        f"timezone: {report['timezone']}",
        f"sourceFilter: {report['sourceFilter']}",
        f"staleMinutes: {report['staleMinutes']}",
        "",
        "Summary",
        f"sessions: {summary['sessionCount']}",
        f"finishedSessions: {summary['finishedSessionCount']}",
        f"unfinishedSessions: {summary['unfinishedSessionCount']}",
        f"activeSessions: {summary['activeSessionCount']}",
        f"staleSessions: {summary['staleSessionCount']}",
        f"actionRequired: {summary['actionRequiredCount']}",
        f"sessionsWithFieldText: {summary['sessionsWithFieldText']}",
        f"sourceDistribution: {json.dumps(summary['sourceDistribution'], ensure_ascii=False)}",
        f"activityDistribution: {json.dumps(summary['activityDistribution'], ensure_ascii=False)}",
        f"actionDistribution: {json.dumps(summary['actionDistribution'], ensure_ascii=False)}",
        f"audioMinutesTotal: {summary['totalAudioMinutes']}",
        f"manualTypingMinutesEstimated: {summary['totalManualTypingMinutes']}",
        f"reviewMinutesEstimated: {summary['totalReviewMinutes']}",
        f"netSavedMinutesEstimated: {summary['totalNetSavedMinutes']}",
        f"estimatedReductionPercent: {summary['estimatedReductionPercent']}",
        f"avgAutomationCoveragePercent: {summary['averageAutomationCoveragePercent']}",
        f"avgFirstTranscriptLatencySeconds: {summary['averageFirstTranscriptLatencySeconds']}",
        f"avgInputRmsDb: {summary['averageAvgInputRmsDb']}",
        f"avgActiveFrameRatio: {summary['averageAvgActiveFrameRatio']}",
        f"technicalErrorsTotal: {summary['totalTechnicalErrors']}",
        "",
        "Action guide",
    ]
    for action, meaning in report["actionGuide"].items():
        lines.append(f"- {action}: {meaning}")
    lines.extend([
        "",
        "Recent sessions",
    ])
    if not report["rows"]:
        lines.append("No consultation sessions found.")
        return "\n".join(lines) + "\n"
    headers = [
        "source",
        "status",
        "activity",
        "finished",
        "age",
        "audioMin",
        "saved",
        "review",
        "coverage",
        "latency",
        "fields",
        "chunks",
        "rmsDb",
        "active",
        "action",
        "sessionId",
    ]
    data_rows = [format_table_row(row) for row in report["rows"]]
    widths = [
        max(len(headers[index]), *(len(item[index]) for item in data_rows))
        for index in range(len(headers))
    ]
    lines.append("  ".join(headers[index].ljust(widths[index]) for index in range(len(headers))))
    lines.append("  ".join("-" * width for width in widths))
    for item in data_rows:
        lines.append("  ".join(item[index].ljust(widths[index]) for index in range(len(headers))))
    return "\n".join(lines) + "\n"


def format_table_row(row: dict) -> list[str]:
    return [
        str(row.get("source") or "-"),
        str(row.get("status") or "-"),
        str(row.get("activityState") or "-"),
        str(row.get("finishedLocal") or "-"),
        minutes_label(row.get("ageMinutes")),
        number_label(row.get("audioMinutes")),
        number_label(row.get("netSavedMinutes")),
        number_label(row.get("reviewMinutes")),
        percent_label(row.get("automationCoveragePercent")),
        seconds_label(row.get("firstTranscriptLatencySeconds")),
        str(row.get("draftFieldsWithText") or 0),
        f"{row.get('doneChunkCount') or 0}/{row.get('chunkCount') or 0}",
        number_label(row.get("avgInputRmsDb")),
        number_label(row.get("avgActiveFrameRatio")),
        str(row.get("recommendedAction") or "-"),
        str(row.get("sessionId") or "-"),
    ]


def classify_activity_state(status: str, processing_state: str, finished_at: str, age_minutes: float, stale_minutes: int) -> str:
    status_lower = str(status or "").strip().lower()
    processing_lower = str(processing_state or "").strip().lower()
    if status_lower == "finished" or processing_lower == "finished" or finished_at:
        return "finished"
    if status_lower == "recording" or processing_lower in {"recording", "processing", "queued", "starting"}:
        return "stale_recording" if age_minutes >= stale_minutes else "active_recording"
    return "unfinished"


def recommended_action_for(activity_state: str, error_count: int, fields_with_text: int) -> str:
    if activity_state == "active_recording":
        return "do_not_clear_live_recording"
    if activity_state == "stale_recording":
        return "recover_or_clear_stale_recording"
    if activity_state == "unfinished":
        return "review_unfinished_session"
    if error_count > 0:
        return "review_technical_errors"
    if fields_with_text > 0:
        return "review_fields_before_lakmus_fill"
    return "no_action_no_field_text"


def action_required(recommended_action: str) -> bool:
    return recommended_action not in {
        "do_not_clear_live_recording",
        "no_action_no_field_text",
    }


def action_guide() -> dict[str, str]:
    return {
        "do_not_clear_live_recording": "Recording looks active; do not clear local state while the consultation may still be running.",
        "recover_or_clear_stale_recording": "Recording has not updated recently; open recorder, export/recover useful text, then finish or clear stale queue.",
        "review_unfinished_session": "Session is not finished and not clearly active; inspect recorder state before starting another consultation.",
        "review_technical_errors": "Chunks have technical errors; do not rely on automation until the recording path is checked.",
        "review_fields_before_lakmus_fill": "Field text exists; clinician should review it before filling visible Lakmus fields.",
        "no_action_no_field_text": "No action required from workflow metrics; no field text was produced for this session.",
    }


def read_safe_session_source(session_dir: Path) -> str:
    status = load_json(session_dir / "status.json")
    metrics = load_json(session_dir / "metrics.json")
    session = load_json(session_dir / "session.json")
    return first_text(
        status.get("sessionSource"),
        metrics.get("sessionSource"),
        status.get("source"),
        metrics.get("source"),
        session.get("source"),
        "microphone",
    )


def source_matches(source: str, source_filter: str) -> bool:
    if not source_filter:
        return True
    normalized_source = normalize_source_filter(source)
    if source_filter == "file_test":
        return normalized_source.startswith("file_test")
    return normalized_source == source_filter


def normalize_source_filter(value: object) -> str:
    source = str(value or "").strip().lower()
    aliases = {
        "mic": "microphone",
        "live": "microphone",
        "recording": "microphone",
        "ready": "readiness_microphone_test",
        "readiness": "readiness_microphone_test",
        "file": "file_test",
    }
    return aliases.get(source, source)


def compact_source_label(value: object) -> str:
    source = normalize_source_filter(value)
    labels = {
        "microphone": "mic",
        "readiness_microphone_test": "ready_test",
        "file_test": "file_test",
        "file_test_probe": "file_probe",
        "file_test_youtube": "file_youtube",
    }
    return labels.get(source, source or "-")


def session_metric_mtime(session_dir: Path) -> float:
    candidates = [session_dir / "metrics.json", session_dir / "status.json", session_dir / "session.json"]
    mtimes = []
    for path in candidates:
        try:
            mtimes.append(path.stat().st_mtime)
        except OSError:
            continue
    if mtimes:
        return max(mtimes)
    return session_dir.stat().st_mtime


def load_json(path: Path) -> dict:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def first_text(*values: object) -> str:
    for value in values:
        text = str(value or "").strip()
        if text:
            return text
    return ""


def first_number(*values: object) -> float | None:
    for value in values:
        try:
            number = float(value)
        except (TypeError, ValueError):
            continue
        if number == number:
            return number
    return None


def int_number(value: float | None) -> int:
    if value is None:
        return 0
    return int(round(value))


def seconds_between(started_at: object, finished_at: object) -> float | None:
    start = parse_iso_datetime(started_at, timezone.utc)
    finish = parse_iso_datetime(finished_at, timezone.utc)
    if not start or not finish:
        return None
    return max(0, (finish - start).total_seconds())


def parse_iso_datetime(value: object, tz) -> datetime | None:
    text = str(value or "").strip()
    if not text:
        return None
    try:
        parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
    except ValueError:
        return None
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(tz)


def local_time(value: object, tz: ZoneInfo) -> str:
    parsed = parse_iso_datetime(value, tz)
    if not parsed:
        return "-"
    return parsed.strftime("%Y-%m-%d %H:%M")


def sum_values(rows: list[dict], key: str) -> float:
    return sum(float(row[key]) for row in rows if isinstance(row.get(key), (int, float)))


def average(rows: list[dict], key: str) -> float | None:
    values = [float(row[key]) for row in rows if isinstance(row.get(key), (int, float))]
    if not values:
        return None
    return sum(values) / len(values)


def round_number(value: object, digits: int) -> float | int | None:
    if value is None:
        return None
    try:
        number = float(value)
    except (TypeError, ValueError):
        return None
    if digits == 0:
        return int(round(number))
    return round(number, digits)


def number_label(value: object) -> str:
    if value is None:
        return "-"
    if isinstance(value, float) and value.is_integer():
        return str(int(value))
    return str(value)


def percent_label(value: object) -> str:
    label = number_label(value)
    return "-" if label == "-" else f"{label}%"


def seconds_label(value: object) -> str:
    label = number_label(value)
    return "-" if label == "-" else f"{label}s"


def minutes_label(value: object) -> str:
    label = number_label(value)
    return "-" if label == "-" else f"{label}m"


if __name__ == "__main__":
    raise SystemExit(main())
