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

import argparse
import csv
import math
import os
import sys
from pathlib import Path


sys.dont_write_bytecode = True
TICK_SECONDS = 1.0 / 254_016_000_000.0
ALTERNATIVE_COLUMNS = (
    "shotId",
    "rank",
    "sequenceStartSeconds",
    "sequenceEndSeconds",
    "decision",
    "sourceId",
    "projectItemRef",
    "sourceStartSeconds",
    "sourceEndSeconds",
    "normalizedScore",
    "placementSourceStartSeconds",
    "placementSourceEndSeconds",
    "eligible",
    "rejectionReason",
)
TRACK_ITEM_COLUMNS = (
    "trackIndex",
    "projectItemRef",
    "sequenceStartSeconds",
    "sequenceEndSeconds",
    "sourceStartSeconds",
    "sourceEndSeconds",
    "disabled",
)
MARKER_COLUMNS = ("comments",)
ACTION_COLUMNS = (
    *ALTERNATIVE_COLUMNS,
    "trackIndex",
    "action",
    "status",
    "message",
    "identity",
)


class AlternativePlanError(ValueError):
    pass


def read_rows(path: Path, columns: tuple[str, ...]) -> list[dict[str, str]]:
    with path.open("r", encoding="utf-8", newline="") as handle:
        reader = csv.DictReader(handle, delimiter="\t", restkey="__extra__")
        if tuple(reader.fieldnames or ()) != columns:
            raise AlternativePlanError(
                f"{path.name} columns must be exactly: {', '.join(columns)}"
            )
        rows: list[dict[str, str]] = []
        for row_number, raw in enumerate(reader, start=2):
            if raw.pop("__extra__", None) is not None:
                raise AlternativePlanError(f"{path.name} row {row_number} contains extra fields")
            row = {name: str(value or "").strip() for name, value in raw.items()}
            if any(row.values()):
                rows.append(row)
        return rows


def parse_int(value: str, label: str) -> int:
    try:
        parsed = int(value)
    except ValueError as exc:
        raise AlternativePlanError(f"{label} must be an integer") from exc
    return parsed


def parse_number(value: str, label: str) -> float:
    try:
        parsed = float(value)
    except ValueError as exc:
        raise AlternativePlanError(f"{label} must be a finite number") from exc
    if not math.isfinite(parsed):
        raise AlternativePlanError(f"{label} must be a finite number")
    return parsed


def parse_bool(value: str, label: str) -> bool:
    normalized = value.strip().lower()
    if normalized == "true":
        return True
    if normalized == "false":
        return False
    raise AlternativePlanError(f"{label} must be true or false")


def within_tick(actual: float, expected: float) -> bool:
    return abs(actual - expected) <= TICK_SECONDS


def overlaps(
    item_start: float,
    item_end: float,
    shot_start: float,
    shot_end: float,
) -> bool:
    half_tick = TICK_SECONDS / 2.0
    return item_start < shot_end - half_tick and item_end > shot_start + half_tick


def alternative_identity(row: dict[str, str], track_index: int) -> str:
    return (
        "automation-agent:ranked-alternative"
        f" | shot={row['shotId']}"
        f" | rank={row['rank']}"
        f" | trackIndex={track_index}"
        f" | source={row['sourceId']}"
        f" | sourceStart={row['sourceStartSeconds']}"
        f" | sourceEnd={row['sourceEndSeconds']}"
        f" | sequenceStart={row['sequenceStartSeconds']}"
        f" | sequenceEnd={row['sequenceEndSeconds']}"
    )


def item_matches_identity(
    item: dict[str, str],
    candidate: dict[str, str],
) -> bool:
    return (
        item["projectItemRef"] == candidate["projectItemRef"]
        and within_tick(
            parse_number(item["sequenceStartSeconds"], "track item sequenceStartSeconds"),
            parse_number(candidate["sequenceStartSeconds"], "candidate sequenceStartSeconds"),
        )
        and within_tick(
            parse_number(item["sequenceEndSeconds"], "track item sequenceEndSeconds"),
            parse_number(candidate["sequenceEndSeconds"], "candidate sequenceEndSeconds"),
        )
        and within_tick(
            parse_number(item["sourceStartSeconds"], "track item sourceStartSeconds"),
            parse_number(candidate["sourceStartSeconds"], "candidate sourceStartSeconds"),
        )
        and within_tick(
            parse_number(item["sourceEndSeconds"], "track item sourceEndSeconds"),
            parse_number(candidate["sourceEndSeconds"], "candidate sourceEndSeconds"),
        )
    )


def build_plan(
    alternatives: list[dict[str, str]],
    track_items: list[dict[str, str]],
    marker_comments: list[str],
    base_track_index: int,
    video_track_count: int,
) -> list[dict[str, str | int]]:
    if base_track_index < 0:
        raise AlternativePlanError("base track index must be >= 0")
    if video_track_count < 0:
        raise AlternativePlanError("video track count must be >= 0")
    identities: set[tuple[str, int]] = set()
    plan: list[dict[str, str | int]] = []
    for row_number, candidate in enumerate(alternatives, start=2):
        shot_id = candidate["shotId"]
        rank = parse_int(candidate["rank"], f"alternatives row {row_number} rank")
        if not shot_id or rank <= 1:
            raise AlternativePlanError(
                f"alternatives row {row_number} requires a shotId and rank greater than one"
            )
        key = (shot_id, rank)
        if key in identities:
            raise AlternativePlanError(
                f"alternatives table contains duplicate shot/rank {shot_id}/{rank}"
            )
        identities.add(key)
        track_index = base_track_index + rank - 2
        identity = alternative_identity(candidate, track_index)
        action = "NONE"
        status = "INELIGIBLE"
        message = candidate["rejectionReason"] or "Candidate is not structurally eligible."
        if parse_bool(candidate["eligible"], f"alternatives row {row_number} eligible"):
            if track_index >= video_track_count:
                status = "MISSING_TRACK"
                message = "The fixed rank lane does not exist. No video track was created."
            else:
                shot_start = parse_number(
                    candidate["sequenceStartSeconds"],
                    f"alternatives row {row_number} sequenceStartSeconds",
                )
                shot_end = parse_number(
                    candidate["sequenceEndSeconds"],
                    f"alternatives row {row_number} sequenceEndSeconds",
                )
                overlapping = [
                    item
                    for item in track_items
                    if parse_int(item["trackIndex"], "track item trackIndex") == track_index
                    and overlaps(
                        parse_number(item["sequenceStartSeconds"], "track item sequenceStartSeconds"),
                        parse_number(item["sequenceEndSeconds"], "track item sequenceEndSeconds"),
                        shot_start,
                        shot_end,
                    )
                ]
                marker_present = any(comment.startswith(identity) for comment in marker_comments)
                owned = [
                    item
                    for item in overlapping
                    if marker_present and item_matches_identity(item, candidate)
                ]
                if len(overlapping) == 1 and len(owned) == 1:
                    if parse_bool(owned[0]["disabled"], "track item disabled"):
                        status = "REUSED"
                        message = (
                            "An identical disabled alternative from an earlier run was "
                            "verified and reused."
                        )
                    else:
                        action = "PLACE"
                        status = "PLACED"
                        message = (
                            "Repair an identical workflow-owned alternative that remained "
                            "enabled by replacing it in place and disabling it."
                        )
                elif overlapping:
                    status = "SKIPPED_OCCUPIED"
                    message = (
                        "The fixed rank lane contains another clip over this shot range; "
                        "nothing was overwritten."
                    )
                else:
                    action = "PLACE"
                    status = "PLACED"
                    message = (
                        "Place frame-accurately on the fixed rank lane, verify, and disable."
                    )
        plan.append(
            {
                **candidate,
                "trackIndex": track_index,
                "action": action,
                "status": status,
                "message": message,
                "identity": identity,
            }
        )
    return plan


def atomic_write_rows(path: Path, rows: list[dict[str, str | int]]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
    try:
        with temporary.open("w", encoding="utf-8", newline="") as handle:
            writer = csv.DictWriter(
                handle,
                fieldnames=ACTION_COLUMNS,
                delimiter="\t",
                lineterminator="\n",
            )
            writer.writeheader()
            writer.writerows(rows)
        os.replace(temporary, path)
    finally:
        temporary.unlink(missing_ok=True)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--alternatives", required=True)
    parser.add_argument("--track-items", required=True)
    parser.add_argument("--markers", required=True)
    parser.add_argument("--base-track-index", type=int, required=True)
    parser.add_argument("--video-track-count", type=int, required=True)
    parser.add_argument("--output", required=True)
    args = parser.parse_args()
    alternatives = read_rows(Path(args.alternatives).resolve(), ALTERNATIVE_COLUMNS)
    track_items = read_rows(Path(args.track_items).resolve(), TRACK_ITEM_COLUMNS)
    markers = read_rows(Path(args.markers).resolve(), MARKER_COLUMNS)
    plan = build_plan(
        alternatives,
        track_items,
        [row["comments"] for row in markers],
        args.base_track_index,
        args.video_track_count,
    )
    output = Path(args.output).resolve()
    atomic_write_rows(output, plan)
    counts: dict[str, int] = {}
    for row in plan:
        status = str(row["status"])
        counts[status] = counts.get(status, 0) + 1
    print(
        "SUMMARY|stage=alternative-plan"
        f"|candidates={len(plan)}"
        f"|place={sum(1 for row in plan if row['action'] == 'PLACE')}"
        f"|reused={counts.get('REUSED', 0)}"
        f"|occupied={counts.get('SKIPPED_OCCUPIED', 0)}"
        f"|missingTrack={counts.get('MISSING_TRACK', 0)}"
        f"|ineligible={counts.get('INELIGIBLE', 0)}"
        f"|output={output}",
        flush=True,
    )
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (AlternativePlanError, OSError) as exc:
        message = str(exc).replace("|", "/").replace("\r", " ").replace("\n", " ")
        print(
            f"ERROR|stage=alternative-plan|message={message}",
            file=sys.stderr,
            flush=True,
        )
        raise SystemExit(2) from exc
