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

import json
import os
import re
import subprocess
from collections import Counter, defaultdict
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

import boto3
from dotenv import load_dotenv
from openpyxl import Workbook
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter


ROOT = Path(__file__).resolve().parents[1]
MCP_DIR = Path(__file__).resolve().parent
ENV_PATH = MCP_DIR / ".env"
LOCAL_GENERATED_DIR = MCP_DIR / "generated"
ANALYTICS_DIR = MCP_DIR / "analytics"
GENERATION_EVENTS_PATH = ANALYTICS_DIR / "generation_events.jsonl"
FAILURE_EVENTS_PATH = ANALYTICS_DIR / "generation_failures.jsonl"
OUTPUT_PATH = ROOT / "IterFact MCP Analytics.xlsx"
RAILWAY_PROJECT_DIR = ROOT
R2_PREFIX_DEFAULT = "generated"
ITERATION_WINDOW = timedelta(hours=12)


def load_settings() -> dict[str, str]:
    load_dotenv(ENV_PATH)
    return {
        "r2_account_id": os.getenv("R2_ACCOUNT_ID", "").strip(),
        "r2_access_key_id": os.getenv("R2_ACCESS_KEY_ID", "").strip(),
        "r2_secret_access_key": os.getenv("R2_SECRET_ACCESS_KEY", "").strip(),
        "r2_bucket": os.getenv("R2_BUCKET", "").strip(),
        "r2_prefix": os.getenv("R2_PREFIX", R2_PREFIX_DEFAULT).strip("/") or R2_PREFIX_DEFAULT,
    }


def run_railway_logs() -> str:
    command = ["cmd", "/c", "npx", "--yes", "@railway/cli", "logs", "-s", "iterfact-mcp", "-e", "production", "-n", "500"]
    try:
        completed = subprocess.run(
            command,
            cwd=str(RAILWAY_PROJECT_DIR),
            check=True,
            capture_output=True,
            text=True,
        )
        return completed.stdout
    except subprocess.CalledProcessError as exc:
        # Railway occasionally returns a non-zero exit code even when it prints usable logs.
        stdout = (exc.stdout or "").strip()
        stderr = (exc.stderr or "").strip()
        if stdout:
            return stdout
        raise RuntimeError(f"Railway log export failed: {stderr or exc}") from exc


def parse_logs(raw_logs: str) -> list[dict[str, Any]]:
    events: list[dict[str, Any]] = []
    current_timestamp: datetime | None = None
    pending_kind: str | None = None

    timestamp_pattern = re.compile(r"^\[(\d{2}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})\]")
    request_type_pattern = re.compile(r"^[A-Za-z]+Request$")
    mcp_post_pattern = re.compile(r'POST /mcp HTTP/1\.1" (\d{3})')
    get_pattern = re.compile(r'GET ([^ ]+) HTTP/1\.1" (\d{3})')

    for raw_line in raw_logs.splitlines():
        line = raw_line.rstrip()
        timestamp_match = timestamp_pattern.match(line)
        if timestamp_match:
            current_timestamp = datetime.strptime(timestamp_match.group(1), "%m/%d/%y %H:%M:%S").replace(tzinfo=timezone.utc)

        if "Processing request of type" in line:
            pending_kind = "request_type"
            continue

        if pending_kind == "request_type":
            stripped = line.strip()
            if stripped and request_type_pattern.match(stripped):
                events.append(
                    {
                        "timestamp_utc": current_timestamp,
                        "event_type": "mcp_request_type",
                        "detail": stripped,
                    }
                )
            if stripped:
                pending_kind = None
            continue

        if "WARNING  Planner JSON parse/validation failed" in line:
            suffix = line.split("failed", 1)[-1].strip()
            events.append(
                {
                    "timestamp_utc": current_timestamp,
                    "event_type": "planner_warning",
                    "detail": suffix or "Planner JSON parse/validation failed",
                }
            )
            continue

        if "HTTP Request: POST" in line:
            events.append(
                {
                    "timestamp_utc": current_timestamp,
                    "event_type": "anthropic_api_call",
                    "detail": "POST https://api.anthropic.com/v1/messages",
                }
            )
            continue

        post_match = mcp_post_pattern.search(line)
        if post_match:
            events.append(
                {
                    "timestamp_utc": current_timestamp,
                    "event_type": "mcp_post",
                    "detail": f"POST /mcp {post_match.group(1)}",
                }
            )
            continue

        get_match = get_pattern.search(line)
        if get_match:
            path, status = get_match.groups()
            event_type = (
                "probe_or_crawler"
                if path.startswith("/wp") or path in {"/robots.txt", "/.env", "/.git/config", "/.vscode/sftp.json", "/%40vite/env"}
                else "http_get"
            )
            events.append(
                {
                    "timestamp_utc": current_timestamp,
                    "event_type": event_type,
                    "detail": f"GET {path} {status}",
                }
            )

    return events


def parse_iso(value: str | None) -> datetime | None:
    if not value:
        return None
    try:
        return datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        return None


def load_jsonl(path: Path) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    if not path.exists():
        return rows
    for line in path.read_text(encoding="utf-8").splitlines():
        if not line.strip():
            continue
        row = json.loads(line)
        rows.append(row)
    return rows


def list_local_artifacts() -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    if not LOCAL_GENERATED_DIR.exists():
        return rows

    for path in sorted(LOCAL_GENERATED_DIR.rglob("*.html")):
        artifact_id = path.parent.name
        rows.append(
            {
                "artifact_id": artifact_id,
                "file_name": path.name,
                "size_bytes": path.stat().st_size,
                "created_utc": datetime.fromtimestamp(path.stat().st_ctime, tz=timezone.utc),
                "local_path": str(path),
            }
        )
    return rows


def list_r2_artifacts(settings: dict[str, str]) -> list[dict[str, Any]]:
    required = [
        settings["r2_account_id"],
        settings["r2_access_key_id"],
        settings["r2_secret_access_key"],
        settings["r2_bucket"],
    ]
    if not all(required):
        return []

    client = boto3.client(
        "s3",
        endpoint_url=f"https://{settings['r2_account_id']}.r2.cloudflarestorage.com",
        aws_access_key_id=settings["r2_access_key_id"],
        aws_secret_access_key=settings["r2_secret_access_key"],
        region_name="auto",
    )

    rows: list[dict[str, Any]] = []
    continuation_token: str | None = None

    while True:
        kwargs: dict[str, Any] = {
            "Bucket": settings["r2_bucket"],
            "Prefix": settings["r2_prefix"],
            "MaxKeys": 1000,
        }
        if continuation_token:
            kwargs["ContinuationToken"] = continuation_token

        response = client.list_objects_v2(**kwargs)
        for item in response.get("Contents", []):
            key = item["Key"]
            parts = key.split("/")
            artifact_id = parts[1] if len(parts) >= 3 else ""
            file_name = parts[-1]
            rows.append(
                {
                    "artifact_id": artifact_id,
                    "key": key,
                    "file_name": file_name,
                    "size_bytes": item["Size"],
                    "last_modified_utc": item["LastModified"],
                }
            )

        if not response.get("IsTruncated"):
            break
        continuation_token = response.get("NextContinuationToken")

    rows.sort(key=lambda row: row["last_modified_utc"])
    return rows


def auto_fit(worksheet) -> None:
    widths: dict[int, int] = {}
    for row in worksheet.iter_rows():
        for cell in row:
            if cell.value is None:
                continue
            widths[cell.column] = max(widths.get(cell.column, 0), len(str(cell.value)))
    for column, width in widths.items():
        worksheet.column_dimensions[get_column_letter(column)].width = min(max(width + 2, 12), 72)


def add_sheet(workbook: Workbook, title: str, headers: list[str], rows: list[list[Any]]) -> None:
    worksheet = workbook.create_sheet(title)
    worksheet.append(headers)
    for cell in worksheet[1]:
        cell.font = Font(bold=True)
    for row in rows:
        worksheet.append(row)
    auto_fit(worksheet)


def infer_iteration_groups(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
    grouped: defaultdict[str, list[dict[str, Any]]] = defaultdict(list)
    for event in events:
        family = event.get("artifact_family_key") or event.get("title") or "unknown"
        grouped[family].append(event)

    rows: list[dict[str, Any]] = []
    for family_key, family_events in grouped.items():
        family_events.sort(key=lambda item: parse_iso(item.get("timestamp_utc")) or datetime.min.replace(tzinfo=timezone.utc))
        previous_timestamp: datetime | None = None
        sequence = 0
        group_index = 0
        for event in family_events:
            event_time = parse_iso(event.get("timestamp_utc"))
            if previous_timestamp is None or event_time is None or (event_time - previous_timestamp) > ITERATION_WINDOW:
                group_index += 1
                sequence = 1
                iteration_type = "new artifact"
            else:
                sequence += 1
                iteration_type = "iteration"

            rows.append(
                {
                    "artifact_family_key": family_key,
                    "iteration_group": f"{family_key}__{group_index}",
                    "iteration_sequence": sequence,
                    "iteration_type": iteration_type,
                    "timestamp_utc": event.get("timestamp_utc"),
                    "artifact_id": event.get("artifact_id"),
                    "title": event.get("title"),
                    "document_format": event.get("document_format"),
                    "theme_name": event.get("theme_name"),
                    "preview_url": event.get("preview_url"),
                }
            )
            previous_timestamp = event_time
    rows.sort(key=lambda row: (row["artifact_family_key"], row["iteration_group"], row["iteration_sequence"]))
    return rows


def build_summary_sheet(
    workbook: Workbook,
    *,
    exported_at: datetime,
    log_events: list[dict[str, Any]],
    local_artifacts: list[dict[str, Any]],
    r2_artifacts: list[dict[str, Any]],
    generation_events: list[dict[str, Any]],
    failure_events: list[dict[str, Any]],
    iteration_rows: list[dict[str, Any]],
) -> None:
    worksheet = workbook.active
    worksheet.title = "Summary"

    mcp_request_types = [event["detail"] for event in log_events if event["event_type"] == "mcp_request_type"]
    request_counts = Counter(mcp_request_types)
    probe_count = sum(1 for event in log_events if event["event_type"] == "probe_or_crawler")
    warning_count = sum(1 for event in log_events if event["event_type"] == "planner_warning")
    anthropic_calls = sum(1 for event in log_events if event["event_type"] == "anthropic_api_call")
    mcp_posts = sum(1 for event in log_events if event["event_type"] == "mcp_post")
    log_timestamps = [event["timestamp_utc"] for event in log_events if event["timestamp_utc"] is not None]
    earliest = min(log_timestamps).isoformat() if log_timestamps else "n/a"
    latest = max(log_timestamps).isoformat() if log_timestamps else "n/a"
    generation_timestamps = [parse_iso(event.get("timestamp_utc")) for event in generation_events]
    generation_timestamps = [timestamp for timestamp in generation_timestamps if timestamp is not None]
    generation_start = min(generation_timestamps).isoformat() if generation_timestamps else "n/a"
    generation_end = max(generation_timestamps).isoformat() if generation_timestamps else "n/a"
    r2_timestamps = [item["last_modified_utc"] for item in r2_artifacts if item.get("last_modified_utc") is not None]
    r2_start = min(r2_timestamps).isoformat() if r2_timestamps else "n/a"
    r2_end = max(r2_timestamps).isoformat() if r2_timestamps else "n/a"

    unique_clients = sorted({event.get("client_id") for event in generation_events if event.get("client_id")})
    total_estimated_cost = round(sum((event.get("estimated_cost_usd") or 0) for event in generation_events), 6)
    average_generation_ms = (
        round(sum((event.get("durations_ms") or {}).get("total", 0) for event in generation_events) / len(generation_events), 1)
        if generation_events
        else None
    )
    engine_counter = Counter()
    for event in generation_events:
        for engine_id in event.get("unique_engine_ids", []):
            engine_counter[engine_id] += 1
    top_engines = ", ".join(f"{engine_id} ({count})" for engine_id, count in engine_counter.most_common(5)) or "n/a"

    rows = [
        ["Exported At (UTC)", exported_at.isoformat()],
        ["What This Export Covers", "Structured MCP generation events, recent Railway runtime logs, local generated HTML files, and R2 artifact objects."],
        ["Important Limitation", "Unique users are not knowable yet because the connector is authless. Any client counts here are anonymous connector/client IDs, not real user accounts."],
        ["Lifetime Tracked Start (UTC)", min(value for value in [generation_start, r2_start] if value != "n/a") if any(value != "n/a" for value in [generation_start, r2_start]) else "n/a"],
        ["Structured Generation Start (UTC)", generation_start],
        ["Structured Generation End (UTC)", generation_end],
        ["R2 Artifact Inventory Start (UTC)", r2_start],
        ["R2 Artifact Inventory End (UTC)", r2_end],
        ["Recent Railway Log Window Start (UTC)", earliest],
        ["Recent Railway Log Window End (UTC)", latest],
        ["Recent MCP CallToolRequest Count", request_counts.get("CallToolRequest", 0)],
        ["Recent MCP POST /mcp Count", mcp_posts],
        ["Recent Anthropic API Calls", anthropic_calls],
        ["Recent Planner Warning Count", warning_count],
        ["Recent Probe/Crawler Count", probe_count],
        ["Structured Generation Event Count", len(generation_events)],
        ["Structured Failure Event Count", len(failure_events)],
        ["Anonymous Client IDs Observed", len(unique_clients)],
        ["Likely New Artifacts", sum(1 for row in iteration_rows if row["iteration_type"] == "new artifact")],
        ["Likely Iterations", sum(1 for row in iteration_rows if row["iteration_type"] == "iteration")],
        ["Estimated Anthropic Planner Cost (USD)", total_estimated_cost],
        ["Average Generation Duration (ms)", average_generation_ms if average_generation_ms is not None else "n/a"],
        ["Top Engine IDs Used", top_engines],
        ["Local Generated HTML Count", len(local_artifacts)],
        ["R2 Artifact Count", len(r2_artifacts)],
        ["R2 Artifact Total Size (MB)", round(sum(item["size_bytes"] for item in r2_artifacts) / 1024 / 1024, 3)],
    ]

    for row_index, row in enumerate(rows, start=1):
        worksheet.append(row)
        worksheet.cell(row=row_index, column=1).font = Font(bold=True)

    auto_fit(worksheet)


def build_workbook(
    *,
    output_path: Path,
    exported_at: datetime,
    raw_logs: str,
    log_events: list[dict[str, Any]],
    local_artifacts: list[dict[str, Any]],
    r2_artifacts: list[dict[str, Any]],
    generation_events: list[dict[str, Any]],
    failure_events: list[dict[str, Any]],
    iteration_rows: list[dict[str, Any]],
) -> None:
    workbook = Workbook()
    build_summary_sheet(
        workbook,
        exported_at=exported_at,
        log_events=log_events,
        local_artifacts=local_artifacts,
        r2_artifacts=r2_artifacts,
        generation_events=generation_events,
        failure_events=failure_events,
        iteration_rows=iteration_rows,
    )

    add_sheet(
        workbook,
        "Generation Events",
        [
            "timestamp_utc",
            "artifact_id",
            "title",
            "document_format",
            "generation_mode",
            "interaction_mode",
            "theme_name",
            "template_match",
            "client_id",
            "planner_model",
            "estimated_cost_usd",
            "planner_repaired",
            "retry_strategy",
            "section_count",
            "unique_engine_count",
            "artifact_family_key",
            "preview_url",
        ],
        [
            [
                event.get("timestamp_utc"),
                event.get("artifact_id"),
                event.get("title"),
                event.get("document_format"),
                event.get("generation_mode"),
                event.get("interaction_mode"),
                event.get("theme_name"),
                event.get("template_match"),
                event.get("client_id"),
                event.get("planner_model"),
                event.get("estimated_cost_usd"),
                event.get("planner_repaired"),
                event.get("retry_strategy"),
                event.get("section_count"),
                event.get("unique_engine_count"),
                event.get("artifact_family_key"),
                event.get("preview_url"),
            ]
            for event in generation_events
        ],
    )

    engine_rows: list[list[Any]] = []
    for event in generation_events:
        for engine in event.get("engines_used", []):
            engine_rows.append(
                [
                    event.get("timestamp_utc"),
                    event.get("artifact_id"),
                    event.get("title"),
                    engine.get("usage_role"),
                    engine.get("engine_id"),
                    engine.get("engine_name"),
                    engine.get("engine_folder"),
                    engine.get("engine_category"),
                    engine.get("instance_id"),
                    engine.get("section_id"),
                    engine.get("section_title"),
                ]
            )
    add_sheet(
        workbook,
        "Engine Usage",
        [
            "timestamp_utc",
            "artifact_id",
            "title",
            "usage_role",
            "engine_id",
            "engine_name",
            "engine_folder",
            "engine_category",
            "instance_id",
            "section_id",
            "section_title",
        ],
        engine_rows,
    )

    add_sheet(
        workbook,
        "Artifact Families",
        [
            "artifact_family_key",
            "iteration_group",
            "iteration_sequence",
            "iteration_type",
            "timestamp_utc",
            "artifact_id",
            "title",
            "document_format",
            "theme_name",
            "preview_url",
        ],
        [
            [
                row["artifact_family_key"],
                row["iteration_group"],
                row["iteration_sequence"],
                row["iteration_type"],
                row["timestamp_utc"],
                row["artifact_id"],
                row["title"],
                row["document_format"],
                row["theme_name"],
                row["preview_url"],
            ]
            for row in iteration_rows
        ],
    )

    add_sheet(
        workbook,
        "Failures",
        ["timestamp_utc", "stage", "error_type", "error_message", "client_id", "prompt_preview"],
        [
            [
                event.get("timestamp_utc"),
                event.get("stage"),
                event.get("error_type"),
                event.get("error_message"),
                event.get("client_id"),
                event.get("prompt_preview"),
            ]
            for event in failure_events
        ],
    )

    cost_by_day: defaultdict[str, float] = defaultdict(float)
    for event in generation_events:
        timestamp = parse_iso(event.get("timestamp_utc"))
        if timestamp is None:
            continue
        cost_by_day[timestamp.date().isoformat()] += float(event.get("estimated_cost_usd") or 0)
    add_sheet(
        workbook,
        "Cost Summary",
        ["day_utc", "estimated_cost_usd"],
        [[day, round(cost, 6)] for day, cost in sorted(cost_by_day.items())],
    )

    add_sheet(
        workbook,
        "Railway Events",
        ["timestamp_utc", "event_type", "detail"],
        [
            [
                event["timestamp_utc"].isoformat() if event["timestamp_utc"] else "",
                event["event_type"],
                event["detail"],
            ]
            for event in log_events
        ],
    )

    add_sheet(
        workbook,
        "R2 Artifacts",
        ["last_modified_utc", "artifact_id", "file_name", "size_bytes", "key"],
        [
            [
                row["last_modified_utc"].isoformat(),
                row["artifact_id"],
                row["file_name"],
                row["size_bytes"],
                row["key"],
            ]
            for row in r2_artifacts
        ],
    )

    add_sheet(
        workbook,
        "Local Artifacts",
        ["created_utc", "artifact_id", "file_name", "size_bytes", "local_path"],
        [
            [
                row["created_utc"].isoformat(),
                row["artifact_id"],
                row["file_name"],
                row["size_bytes"],
                row["local_path"],
            ]
            for row in local_artifacts
        ],
    )

    add_sheet(
        workbook,
        "Raw Railway Logs",
        ["line"],
        [[line] for line in raw_logs.splitlines()],
    )

    workbook.save(output_path)


def main() -> None:
    settings = load_settings()
    exported_at = datetime.now(timezone.utc)

    raw_logs = run_railway_logs()
    log_events = parse_logs(raw_logs)
    local_artifacts = list_local_artifacts()
    r2_artifacts = list_r2_artifacts(settings)
    generation_events = load_jsonl(GENERATION_EVENTS_PATH)
    failure_events = load_jsonl(FAILURE_EVENTS_PATH)
    iteration_rows = infer_iteration_groups(generation_events)

    build_workbook(
        output_path=OUTPUT_PATH,
        exported_at=exported_at,
        raw_logs=raw_logs,
        log_events=log_events,
        local_artifacts=local_artifacts,
        r2_artifacts=r2_artifacts,
        generation_events=generation_events,
        failure_events=failure_events,
        iteration_rows=iteration_rows,
    )
    print(
        json.dumps(
            {
                "saved_to": str(OUTPUT_PATH),
                "structured_generation_events": len(generation_events),
                "failure_events": len(failure_events),
                "r2_artifact_count": len(r2_artifacts),
                "local_artifact_count": len(local_artifacts),
                "recent_log_event_count": len(log_events),
            },
            indent=2,
        )
    )


if __name__ == "__main__":
    main()
