from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping

from caption_profile import CaptionProfile
from portable_profile import (
    SETTING_LABELS,
    PortableCaptionProfile,
    ResolvedProfile,
    resolve_profile,
)
from timebase import RationalTimebase


REFLOW_TOOL_DEFAULTS = {
    "layout.maxUnitsPerLine": 42,
    "layout.maxLines": 2,
    "timing.minDurationMs": 1000,
    "timing.maxDurationMs": 7000,
}
REFLOW_APPLICABLE_KEYS = frozenset(REFLOW_TOOL_DEFAULTS)
REFLOW_MEASURED_ONLY_KEYS = frozenset(
    {
        "readability.maxCps",
        "timing.minGapMs",
        "timing.minTimestampGapFrames",
    }
)

CONDENSATION_TOOL_DEFAULTS = {
    **REFLOW_TOOL_DEFAULTS,
    "readability.maxCps": 17.0,
}
CONDENSATION_APPLICABLE_KEYS = frozenset(CONDENSATION_TOOL_DEFAULTS)

REFERENCE_TOOL_DEFAULTS = dict(CONDENSATION_TOOL_DEFAULTS)
REFERENCE_APPLICABLE_KEYS = frozenset(REFERENCE_TOOL_DEFAULTS)

SOURCE_LABELS = {
    "TOOL_DEFAULT": "Standard setting",
    "PROFILE": "Selected profile",
    "RUN_OVERRIDE": "Changed for this run",
}


@dataclass(frozen=True)
class ResolvedWorkflowProfile:
    selected_profile: PortableCaptionProfile
    resolved_profile: ResolvedProfile
    processing_profile: CaptionProfile
    measured_only_keys: tuple[str, ...] = ()
    measurement_context_keys: tuple[str, ...] = ()
    gap_timebase: RationalTimebase | None = None
    required_gap_frames: int | None = None


def standard_workflow_profile() -> PortableCaptionProfile:
    return PortableCaptionProfile(name="Standard settings", settings={})


def _resolve_workflow_profile(
    selected_profile: PortableCaptionProfile,
    *,
    tool_defaults: Mapping[str, Any],
    applicable_keys: frozenset[str],
    reviewed_values: Mapping[str, Any] | None = None,
    measure_reflow_rules: bool = False,
) -> ResolvedWorkflowProfile:
    initial = resolve_profile(
        selected_profile,
        tool_defaults=tool_defaults,
        applicable_keys=applicable_keys,
    )
    run_overrides = {
        key: value
        for key, value in (reviewed_values or {}).items()
        if value is not None and initial.effective_settings.get(key) != value
    }
    resolved = resolve_profile(
        selected_profile,
        tool_defaults=tool_defaults,
        run_overrides=run_overrides,
        applicable_keys=applicable_keys,
    )
    settings = resolved.effective_settings
    max_cps = float(settings.get("readability.maxCps", 17.0))
    preferred_gap_ms = 80
    measured_only_keys: list[str] = []
    measurement_context_keys: list[str] = []
    gap_timebase: RationalTimebase | None = None
    required_gap_frames: int | None = None
    if measure_reflow_rules:
        measured_profile_keys = REFLOW_MEASURED_ONLY_KEYS.intersection(
            selected_profile.settings
        )
        if "readability.maxCps" in measured_profile_keys:
            max_cps = float(selected_profile.settings["readability.maxCps"])
            measured_only_keys.append("readability.maxCps")
        if "timing.minGapMs" in measured_profile_keys:
            preferred_gap_ms = int(selected_profile.settings["timing.minGapMs"])
            measured_only_keys.append("timing.minGapMs")
        elif "timing.minTimestampGapFrames" in measured_profile_keys:
            gap_timebase = RationalTimebase.from_json(
                selected_profile.settings["timing.timebase"]
            )
            required_gap_frames = int(
                selected_profile.settings["timing.minTimestampGapFrames"]
            )
            preferred_gap_ms = gap_timebase.minimum_timestamp_gap_ms(
                required_gap_frames
            )
            measured_only_keys.append("timing.minTimestampGapFrames")
            measurement_context_keys.append("timing.timebase")
    processing_profile = CaptionProfile(
        name=selected_profile.name,
        max_units_per_line=int(settings["layout.maxUnitsPerLine"]),
        max_lines=int(settings["layout.maxLines"]),
        max_cps=max_cps,
        min_duration_ms=int(settings["timing.minDurationMs"]),
        max_duration_ms=int(settings["timing.maxDurationMs"]),
        # Reflow does not force a delivery gap yet. A stored requirement is
        # nevertheless measured against the result so the report does not
        # substitute an unrelated general preference.
        preferred_gap_ms=preferred_gap_ms,
    )
    processing_profile.validate()
    return ResolvedWorkflowProfile(
        selected_profile=selected_profile,
        resolved_profile=resolved,
        processing_profile=processing_profile,
        measured_only_keys=tuple(sorted(measured_only_keys)),
        measurement_context_keys=tuple(sorted(measurement_context_keys)),
        gap_timebase=gap_timebase,
        required_gap_frames=required_gap_frames,
    )


def resolve_reflow_profile(
    selected_profile: PortableCaptionProfile,
    *,
    reviewed_values: Mapping[str, Any] | None = None,
) -> ResolvedWorkflowProfile:
    return _resolve_workflow_profile(
        selected_profile,
        tool_defaults=REFLOW_TOOL_DEFAULTS,
        applicable_keys=REFLOW_APPLICABLE_KEYS,
        reviewed_values=reviewed_values,
        measure_reflow_rules=True,
    )


def resolve_condensation_profile(
    selected_profile: PortableCaptionProfile,
    *,
    reviewed_values: Mapping[str, Any] | None = None,
) -> ResolvedWorkflowProfile:
    return _resolve_workflow_profile(
        selected_profile,
        tool_defaults=CONDENSATION_TOOL_DEFAULTS,
        applicable_keys=CONDENSATION_APPLICABLE_KEYS,
        reviewed_values=reviewed_values,
    )


def resolve_reference_profile(
    selected_profile: PortableCaptionProfile,
    *,
    reviewed_values: Mapping[str, Any] | None = None,
) -> ResolvedWorkflowProfile:
    return _resolve_workflow_profile(
        selected_profile,
        tool_defaults=REFERENCE_TOOL_DEFAULTS,
        applicable_keys=REFERENCE_APPLICABLE_KEYS,
        reviewed_values=reviewed_values,
    )


def workflow_review_form(resolution: ResolvedWorkflowProfile) -> dict[str, Any]:
    settings = resolution.resolved_profile.effective_settings
    result = {
        "profileName": resolution.selected_profile.name,
        "maxUnitsPerLine": settings["layout.maxUnitsPerLine"],
        "maxLines": settings["layout.maxLines"],
        "minDurationMs": settings["timing.minDurationMs"],
        "maxDurationMs": settings["timing.maxDurationMs"],
    }
    if "readability.maxCps" in settings:
        result["maxCps"] = settings["readability.maxCps"]
    return result


def _display_setting(
    key: str,
    value: Any,
    *,
    resolution: ResolvedWorkflowProfile | None = None,
) -> str:
    if key == "readability.maxCps":
        return f"{value:g} characters per second"
    if key in {"timing.minDurationMs", "timing.maxDurationMs"}:
        return f"{value} ms"
    if key == "timing.minGapMs":
        return f"{value} ms"
    if key == "timing.minTimestampGapFrames":
        if resolution is not None and resolution.gap_timebase is not None:
            return (
                f"{value} frame(s), at least "
                f"{resolution.gap_timebase.minimum_timestamp_gap_ms(int(value))} ms "
                "in SRT timestamps"
            )
        return f"{value} frame(s)"
    return str(value)


def workflow_settings_report(
    resolution: ResolvedWorkflowProfile,
    *,
    profile_path: Path | None = None,
) -> dict[str, Any]:
    resolved = resolution.resolved_profile
    is_standard_run = (
        profile_path is None
        and resolution.selected_profile.name == "Standard settings"
        and not resolution.selected_profile.settings
    )
    if is_standard_run:
        profile_result = "No caption profile was selected; Standard settings were used."
    elif resolved.overridden_profile_keys:
        profile_result = (
            "Supported values from the selected profile were applied except for "
            "values changed for this run."
        )
    elif resolved.applied_profile_keys:
        profile_result = "All profile values this tool can apply were applied."
    elif resolution.measured_only_keys:
        profile_result = "The selected profile is used for result measurements only."
    else:
        profile_result = "The selected profile contains no values used by this tool."
    if resolution.measured_only_keys and resolved.applied_profile_keys:
        profile_result += (
            " Reading-speed and gap values listed as measured are checked in "
            "the result, but Reflow does not change wording or force gaps to "
            "meet them."
        )
    elif resolution.measured_only_keys:
        profile_result += (
            " Reading-speed and gap requirements are checked in the result, "
            "but Reflow does not change wording or force gaps to meet them."
        )
    measured_keys = set(resolution.measured_only_keys)
    measurement_context_keys = set(resolution.measurement_context_keys)
    not_checked_keys = [
        key
        for key in resolved.unsupported_profile_keys
        if key not in measured_keys and key not in measurement_context_keys
    ]
    return {
        "profileName": resolution.selected_profile.name,
        "profilePath": str(profile_path) if profile_path is not None else "",
        "effective": [
            {
                "key": key,
                "label": SETTING_LABELS[key],
                "value": value,
                "displayValue": _display_setting(key, value, resolution=resolution),
                "source": resolved.sources[key],
                "sourceLabel": SOURCE_LABELS[resolved.sources[key]],
            }
            for key, value in resolved.effective_settings.items()
        ],
        "appliedProfileKeys": list(resolved.applied_profile_keys),
        "overriddenProfileKeys": list(resolved.overridden_profile_keys),
        "measuredNotEnforced": [
            {
                "key": key,
                "label": SETTING_LABELS[key],
                "value": resolution.selected_profile.settings[key],
                "displayValue": _display_setting(
                    key,
                    resolution.selected_profile.settings[key],
                    resolution=resolution,
                ),
                "source": "PROFILE",
                "sourceLabel": SOURCE_LABELS["PROFILE"],
            }
            for key in resolution.measured_only_keys
        ],
        "notChecked": [
            {"key": key, "label": SETTING_LABELS[key]}
            for key in not_checked_keys
        ],
        "profileRuleResult": profile_result,
    }


def add_measurement_results(
    caption_settings: dict[str, Any],
    findings: list[Any],
) -> None:
    finding_codes = {str(getattr(item, "code", "")) for item in findings}
    codes_by_key = {
        "readability.maxCps": {"CPS_TOO_HIGH"},
        "timing.minGapMs": {"GAP_BELOW_PROFILE_LIMIT"},
        "timing.minTimestampGapFrames": {"GAP_BELOW_PROFILE_LIMIT"},
    }
    measured = caption_settings.get("measuredNotEnforced")
    if not isinstance(measured, list) or not measured:
        return
    violations = 0
    for item in measured:
        if not isinstance(item, dict):
            continue
        violated = bool(codes_by_key.get(str(item.get("key")), set()) & finding_codes)
        item["violated"] = violated
        violations += int(violated)
    if violations:
        caption_settings["measurementResult"] = (
            "The reflowed SRT does not meet every measured profile requirement. "
            "Reflow reports these results but does not change wording or force "
            "gaps to make them pass."
        )
    else:
        caption_settings["measurementResult"] = (
            "The reflowed SRT meets the profile requirements measured by this "
            "tool. This is a measurement only; Reflow does not change wording "
            "or force gaps to make requirements pass."
        )
