from __future__ import annotations

import json
import hashlib
import math
import statistics
from pathlib import Path
from typing import Any

from caption_profile import CaptionProfile, characters_per_second, display_unit_count
from operations import Finding
from timed_text import Cue, TimedTextDocument, TimingFidelity


COUNTING_METHOD = (
    "Unicode base-code-point approximation; supported markup, combining marks, ZWJ, and variation "
    "selectors are excluded; CPS also excludes whitespace."
)

FINDING_LABELS = {
    "CPS_TOO_HIGH": "Reading Speed Too High",
    "LINE_TOO_LONG": "Line Too Long",
    "TOO_MANY_LINES": "Too Many Lines",
    "CUE_TOO_SHORT": "Caption Display Time Too Short",
    "CUE_TOO_LONG": "Caption Display Time Too Long",
    "CUE_OVERLAP": "Overlapping Captions",
    "GAP_BELOW_PREFERENCE": "Gap Between Captions Too Short",
    "GAP_BELOW_PROFILE_LIMIT": "Required Gap Between Captions Not Met",
    "START_NOT_FRAME_ALIGNED": "Caption Start Is Between Frames",
    "END_NOT_FRAME_ALIGNED": "Caption End Is Between Frames",
    "SUSPICIOUS_DUPLICATE_TEXT": "Repeated Caption Text",
    "INVALID_DOCUMENT": "Invalid SRT Structure",
    "MARKUP_WARNING": "Caption Formatting Needs Review",
}


def finding_label(code: str) -> str:
    return FINDING_LABELS.get(code, code.replace("_", " ").title())


def findings_to_json(
    findings: list[Finding],
    *,
    status: str,
    timing_fidelity: str,
    extra: dict[str, Any] | None = None,
) -> dict[str, Any]:
    counts: dict[str, int] = {}
    for finding in findings:
        counts[finding.severity] = counts.get(finding.severity, 0) + 1
    return {
        "version": 1,
        "status": status,
        "timingFidelity": timing_fidelity,
        "displayUnitCountingMethod": COUNTING_METHOD,
        "counts": counts,
        "findings": [
            {
                "severity": finding.severity,
                "code": finding.code,
                "message": finding.message,
                "cueIds": list(finding.cue_ids),
            }
            for finding in findings
        ],
        **(extra or {}),
    }


def findings_to_markdown(report: dict[str, Any], *, title: str) -> str:
    lines = [
        f"# {title}",
        "",
        f"- Status: `{report['status']}`",
        f"- Timing fidelity: `{report['timingFidelity']}`",
        f"- Display-unit counting: {report['displayUnitCountingMethod']}",
        "",
    ]
    analysis_metrics_value = report.get("analysisMetrics")
    if isinstance(analysis_metrics_value, dict):
        lines.extend(["## Overview", ""])
        lines.append(f"- Captions checked: {analysis_metrics_value['cueCount']}")
        duration_ms = analysis_metrics_value.get("durationMs")
        if duration_ms:
            lines.append(
                "- Caption duration: "
                f"{duration_ms['minimum']} ms minimum, "
                f"{duration_ms['typical']:.0f} ms typical, "
                f"{duration_ms['maximum']} ms maximum"
            )
        gap_ms = analysis_metrics_value.get("adjacentGapMs")
        if gap_ms:
            lines.append(
                "- Adjacent-caption gaps: "
                f"{gap_ms['minimum']} ms minimum, "
                f"{gap_ms['typical']:.0f} ms typical, "
                f"{gap_ms['maximum']} ms maximum"
            )
        lines.append(
            "- Gap types: "
            f"{analysis_metrics_value['overlapCount']} overlap(s), "
            f"{analysis_metrics_value['zeroGapCount']} zero-length gap(s), "
            f"{analysis_metrics_value['belowRequiredGapCount']} below the "
            "selected requirement"
        )
        if analysis_metrics_value.get("frameAlignment"):
            alignment = analysis_metrics_value["frameAlignment"]
            lines.append(
                "- Frame boundaries: "
                f"{alignment['nonAlignedStarts']} start(s) and "
                f"{alignment['nonAlignedEnds']} end(s) between frames"
            )
        lines.append("")
    caption_settings = report.get("captionSettings")
    if isinstance(caption_settings, dict):
        lines.extend(["## Settings checked", ""])
        profile_name = caption_settings.get("profileName", "Standard settings")
        lines.append(f"- Selected settings: {profile_name}")
        for item in caption_settings.get("effective", []):
            lines.append(
                f"- {item['label']}: {item['displayValue']} "
                f"({item['sourceLabel']})"
            )
        measured = caption_settings.get("measuredNotEnforced", [])
        if measured:
            lines.append("- Profile requirements measured after Reflow:")
            for item in measured:
                if "violated" not in item:
                    outcome = "not measured because no completed Reflow result is available"
                elif item.get("violated"):
                    outcome = "does not meet requirement"
                else:
                    outcome = "meets requirement"
                lines.append(
                    f"  - {item['label']}: {item['displayValue']} "
                    f"({outcome}; measured, not enforced by Reflow)"
                )
        measurement_result = caption_settings.get("measurementResult")
        if measurement_result:
            lines.append(f"- Measurement result: {measurement_result}")
        unsupported = caption_settings.get("notChecked", [])
        if unsupported:
            lines.append(
                "- Stored profile settings not checked by this tool: "
                + ", ".join(item["label"] for item in unsupported)
            )
        pass_message = caption_settings.get("profileRuleResult")
        if pass_message:
            lines.append(f"- Profile-rule result: {pass_message}")
        lines.append("")
    translation_task = report.get("translationTask")
    if isinstance(translation_task, dict):
        lines.extend(["## Language task", ""])
        lines.append(f"- Approach: {translation_task['approachLabel']}")
        editor_instructions = translation_task.get("editorInstructions")
        lines.append(
            "- Additional wording instructions: "
            + (editor_instructions if editor_instructions else "None")
        )
        protected_terms = translation_task.get("protectedTerms", [])
        lines.append(
            "- Source text required to stay exactly unchanged: "
            + (", ".join(protected_terms) if protected_terms else "None")
        )
        unused_protected_terms = translation_task.get("unusedProtectedTerms", [])
        if unused_protected_terms:
            lines.append(
                "- Requested protected text not found in the source: "
                + ", ".join(unused_protected_terms)
            )
        if translation_task.get("humanTargetMarketReviewRequired"):
            lines.append("- Human review: Required from someone familiar with the target market")
        lines.append("")
    lines.extend(["## Findings", ""])
    findings = report.get("findings", [])
    if not findings:
        lines.append("No findings.")
    else:
        for finding in findings:
            cue_text = f" ({', '.join(finding['cueIds'])})" if finding.get("cueIds") else ""
            lines.append(
                f"- **{finding['severity']} — {finding['code']}**{cue_text}: {finding['message']}"
            )
    return "\n".join(lines) + "\n"


def write_json(path: Path, value: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def _format_clock(milliseconds: int) -> str:
    seconds, ms = divmod(max(0, milliseconds), 1000)
    minutes, second = divmod(seconds, 60)
    hours, minute = divmod(minutes, 60)
    return f"{hours:02d}:{minute:02d}:{second:02d},{ms:03d}"


def analysis_overview(
    document: TimedTextDocument,
    findings: list[Finding],
    profile: CaptionProfile,
    *,
    caption_settings: dict[str, Any] | None = None,
    timebase=None,
) -> str:
    cps_values = [
        characters_per_second(cue.text, cue.end_ms - cue.start_ms)
        for cue in document.cues
    ]
    line_lengths = [
        display_unit_count(line)
        for cue in document.cues
        for line in (cue.text.splitlines() or [cue.text])
    ]
    counts: dict[str, int] = {}
    for finding in findings:
        if finding.severity != "INFO":
            counts[finding.code] = counts.get(finding.code, 0) + 1
    sorted_cps = sorted(cps_values)
    p90_index = max(0, math.ceil(len(sorted_cps) * 0.9) - 1)
    durations = [cue.end_ms - cue.start_ms for cue in document.cues]
    gaps = [
        current.start_ms - previous.end_ms
        for previous, current in zip(document.cues, document.cues[1:])
    ]
    lines = [
        f"Captions checked: {len(document.cues)}",
        f"Problems found: {sum(counts.values())}",
        "",
        (
            "Reading speed: "
            f"typical {statistics.median(cps_values):.1f} CPS, "
            f"90% at or below {sorted_cps[p90_index]:.1f} CPS, "
            f"maximum {max(cps_values):.1f} CPS"
            if cps_values
            else "Reading speed: no captions"
        ),
        (
            f"Longest line: {max(line_lengths)} text units "
            f"(configured maximum: {profile.max_units_per_line})"
            if line_lengths
            else "Longest line: no caption text"
        ),
        (
            "Caption duration: "
            f"minimum {min(durations)} ms, typical "
            f"{statistics.median(durations):.0f} ms, maximum {max(durations)} ms"
            if durations
            else "Caption duration: no captions"
        ),
        (
            "Adjacent-caption gap: "
            f"minimum {min(gaps)} ms, typical "
            f"{statistics.median(gaps):.0f} ms, maximum {max(gaps)} ms"
            if gaps
            else "Adjacent-caption gap: no adjacent captions"
        ),
        "",
    ]
    if timebase is not None and durations:
        duration_frames = [
            float(timebase.milliseconds_to_frames(duration))
            for duration in durations
        ]
        non_aligned_starts = sum(
            not timebase.is_frame_aligned(cue.start_ms) for cue in document.cues
        )
        non_aligned_ends = sum(
            not timebase.is_frame_aligned(cue.end_ms) for cue in document.cues
        )
        lines.extend(
            [
                (
                    "Caption duration in frames: "
                    f"minimum {min(duration_frames):.2f}, typical "
                    f"{statistics.median(duration_frames):.2f}, maximum "
                    f"{max(duration_frames):.2f}"
                ),
                (
                    "Frame-boundary check: "
                    f"{non_aligned_starts} start(s) and {non_aligned_ends} end(s) "
                    "fall between delivery frames"
                ),
                "",
            ]
        )
    if caption_settings is not None:
        lines.append(
            "Settings checked: "
            + str(caption_settings.get("profileName", "Standard settings"))
        )
        for item in caption_settings.get("effective", []):
            lines.append(
                f"- {item['label']}: {item['displayValue']} "
                f"({item['sourceLabel']})"
            )
        not_checked = caption_settings.get("notChecked", [])
        if not_checked:
            lines.append(
                "- Stored profile settings not checked here: "
                + ", ".join(item["label"] for item in not_checked)
            )
        lines.extend(["", "Problems by type:"])
    else:
        lines.append("Problems by type:")
    if counts:
        for code, count in sorted(counts.items(), key=lambda item: (-item[1], item[0])):
            lines.append(f"- {finding_label(code)}: {count}")
    else:
        lines.append("- None")
    cue_by_id = {cue.id: cue for cue in document.cues}
    located = [
        finding
        for finding in findings
        if finding.severity != "INFO"
        and finding.cue_ids
        and finding.cue_ids[0] in cue_by_id
    ]
    if located:
        lines.extend(["", "Most important locations:"])
        for finding in sorted(
            located,
            key=lambda item: (
                0 if item.severity == "ERROR" else 1,
                cue_by_id[item.cue_ids[0]].start_ms,
            ),
        )[:10]:
            cue = cue_by_id[finding.cue_ids[0]]
            lines.append(
                f"- {_format_clock(cue.start_ms)} — "
                f"{finding_label(finding.code)}: {finding.message}"
            )
        if len(located) > 10:
            lines.append(
                f"- …and {len(located) - 10} more locations in the full report."
            )
    return "\n".join(lines) + "\n"


def analysis_metrics(
    document: TimedTextDocument,
    findings: list[Finding],
    *,
    timebase=None,
) -> dict[str, Any]:
    durations = [cue.end_ms - cue.start_ms for cue in document.cues]
    gaps = [
        current.start_ms - previous.end_ms
        for previous, current in zip(document.cues, document.cues[1:])
    ]
    result: dict[str, Any] = {
        "cueCount": len(document.cues),
        "adjacentBoundaryCount": len(gaps),
        "overlapCount": sum(
            finding.code == "CUE_OVERLAP" for finding in findings
        ),
        "zeroGapCount": sum(gap == 0 for gap in gaps),
        "belowRequiredGapCount": sum(
            finding.code == "GAP_BELOW_PROFILE_LIMIT" for finding in findings
        ),
        "belowPreferredGapCount": sum(
            finding.code == "GAP_BELOW_PREFERENCE" for finding in findings
        ),
        "durationMs": (
            {
                "minimum": min(durations),
                "typical": statistics.median(durations),
                "maximum": max(durations),
            }
            if durations
            else None
        ),
        "adjacentGapMs": (
            {
                "minimum": min(gaps),
                "typical": statistics.median(gaps),
                "maximum": max(gaps),
            }
            if gaps
            else None
        ),
    }
    if timebase is not None:
        duration_frames = [
            float(timebase.milliseconds_to_frames(duration))
            for duration in durations
        ]
        gap_frames = [
            float(timebase.milliseconds_to_frames(gap))
            for gap in gaps
            if gap >= 0
        ]
        result["durationFrames"] = (
            {
                "minimum": min(duration_frames),
                "typical": statistics.median(duration_frames),
                "maximum": max(duration_frames),
            }
            if duration_frames
            else None
        )
        result["adjacentGapFrames"] = (
            {
                "minimum": min(gap_frames),
                "typical": statistics.median(gap_frames),
                "maximum": max(gap_frames),
            }
            if gap_frames
            else None
        )
        result["frameAlignment"] = {
            "nonAlignedStarts": sum(
                not timebase.is_frame_aligned(cue.start_ms)
                for cue in document.cues
            ),
            "nonAlignedEnds": sum(
                not timebase.is_frame_aligned(cue.end_ms)
                for cue in document.cues
            ),
        }
    return result


def review_marker_document(
    document: TimedTextDocument,
    findings: list[Finding],
    *,
    source_identity: str,
) -> TimedTextDocument:
    cue_by_id = {cue.id: cue for cue in document.cues}
    parent = {cue.id: cue.id for cue in document.cues}

    def find(value: str) -> str:
        while parent[value] != value:
            parent[value] = parent[parent[value]]
            value = parent[value]
        return value

    def union(left: str, right: str) -> None:
        left_root = find(left)
        right_root = find(right)
        if left_root != right_root:
            parent[right_root] = left_root

    for finding in findings:
        if finding.code == "CUE_OVERLAP":
            valid_ids = [cue_id for cue_id in finding.cue_ids if cue_id in cue_by_id]
            for cue_id in valid_ids[1:]:
                union(valid_ids[0], cue_id)

    grouped: dict[str, list[Finding]] = {}
    for finding in findings:
        valid_ids = [cue_id for cue_id in finding.cue_ids if cue_id in cue_by_id]
        if finding.severity == "INFO" or not valid_ids:
            continue
        grouped.setdefault(find(valid_ids[0]), []).append(finding)

    source_tag = hashlib.sha256(source_identity.encode("utf-8")).hexdigest()[:12]
    markers: list[Cue] = []
    for group_index, (root, group_findings) in enumerate(
        sorted(grouped.items(), key=lambda item: cue_by_id[item[0]].start_ms),
        start=1,
    ):
        member_ids = {
            cue_id
            for finding in group_findings
            for cue_id in finding.cue_ids
            if cue_id in cue_by_id and find(cue_id) == root
        }
        if not member_ids:
            member_ids = {root}
        members = sorted(
            (cue_by_id[cue_id] for cue_id in member_ids),
            key=lambda cue: (cue.start_ms, cue.end_ms),
        )
        codes = list(dict.fromkeys(finding.code for finding in group_findings))
        readable_codes = [finding_label(code) for code in codes]
        title = "SRT review: " + " + ".join(readable_codes[:3])
        if len(readable_codes) > 3:
            title += f" + {len(readable_codes) - 3} more"
        ownership = (
            f"AA:SRT-READABILITY:v1:{source_tag}:location-{group_index:06d}"
        )
        captions = "\n".join(
            f"[{_format_clock(cue.start_ms)}] {cue.text.replace(chr(10), ' ')}"
            for cue in members
        )
        issue_lines = "\n".join(
            f"- {finding_label(finding.code)}: {finding.message}"
            for finding in group_findings
        )
        markers.append(
            Cue(
                id=f"cue_{group_index:06d}",
                srt_index=group_index,
                start_ms=min(cue.start_ms for cue in members),
                end_ms=max(cue.end_ms for cue in members),
                text=(
                    f"{title}\n{ownership}\nSource SRT: {source_identity}\n"
                    f"Caption text:\n{captions}\nProblems:\n{issue_lines}"
                ),
            )
        )
    return TimedTextDocument(
        language=document.language,
        source_kind="srt-analysis-review-markers",
        source_name=document.source_name,
        timing_fidelity=TimingFidelity.CUE_ALIGNED_ONLY,
        cues=markers,
    )
