"""Pure exact requirement coverage calculation."""

from __future__ import annotations

from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Literal


class ExactCoverageError(ValueError):
    """Raised when coverage input cannot define a valid comparison."""


@dataclass(frozen=True)
class ExactCoverageResult:
    coverage_percent: float
    scope_precision_percent: float
    covered_count: int
    total_count: int
    unmapped_commitments: tuple[str, ...]
    contradicted_requirements: tuple[str, ...]
    verdict: Literal["exact", "under", "over", "contradicted"]

    def as_report_summary(self) -> dict[str, object]:
        return {
            "coveragePercent": self.coverage_percent,
            "scopePrecisionPercent": self.scope_precision_percent,
            "coveredCount": self.covered_count,
            "totalCount": self.total_count,
            "unmappedCommitments": list(self.unmapped_commitments),
            "contradictedRequirements": list(self.contradicted_requirements),
            "coverageVerdict": self.verdict,
        }


def calculate_exact_coverage(
    original_requirement_ids: Sequence[str],
    coverage_status_by_id: Mapping[str, str],
    commitment_requirement_ids: Mapping[str, Sequence[str]],
) -> ExactCoverageResult:
    original_ids = tuple(original_requirement_ids)
    if not original_ids:
        raise ExactCoverageError("original requirement denominator is empty")
    if len(set(original_ids)) != len(original_ids):
        raise ExactCoverageError("original requirement ids contain duplicates")

    original_id_set = set(original_ids)
    if set(coverage_status_by_id) != original_id_set:
        raise ExactCoverageError("coverage rows must match original requirement ids")
    allowed_statuses = {"covered", "not-covered", "contradicted"}
    if any(status not in allowed_statuses for status in coverage_status_by_id.values()):
        raise ExactCoverageError("coverage status is invalid")
    if not commitment_requirement_ids:
        raise ExactCoverageError("implementation commitment denominator is empty")

    unmapped = tuple(
        commitment_id
        for commitment_id, requirement_ids in commitment_requirement_ids.items()
        if not requirement_ids
        or any(requirement_id not in original_id_set for requirement_id in requirement_ids)
    )
    contradicted = tuple(
        requirement_id
        for requirement_id in original_ids
        if coverage_status_by_id[requirement_id] == "contradicted"
    )
    covered_count = sum(
        coverage_status_by_id[requirement_id] == "covered"
        for requirement_id in original_ids
    )
    coverage_percent = round(covered_count / len(original_ids) * 100, 2)
    traceable_count = len(commitment_requirement_ids) - len(unmapped)
    scope_precision_percent = round(
        traceable_count / len(commitment_requirement_ids) * 100,
        2,
    )
    verdict: Literal["exact", "under", "over", "contradicted"]
    if contradicted:
        verdict = "contradicted"
    elif covered_count != len(original_ids):
        verdict = "under"
    elif unmapped:
        verdict = "over"
    else:
        verdict = "exact"

    return ExactCoverageResult(
        coverage_percent=coverage_percent,
        scope_precision_percent=scope_precision_percent,
        covered_count=covered_count,
        total_count=len(original_ids),
        unmapped_commitments=unmapped,
        contradicted_requirements=contradicted,
        verdict=verdict,
    )


def calculate_plan_exact_coverage(
    original_requirement_ids: Sequence[str],
    coverage_status_by_id: Mapping[str, str],
    stage_requirement_ids: Mapping[int, Sequence[str]],
    file_requirement_ids: Mapping[str, Sequence[str]],
) -> tuple[ExactCoverageResult, tuple[int, ...], tuple[str, ...]]:
    """Apply exact coverage to the executable stage and file-change scope."""
    commitments = {
        **{
            f"stage:{stage}": requirement_ids
            for stage, requirement_ids in stage_requirement_ids.items()
        },
        **{
            f"file:{path}": requirement_ids
            for path, requirement_ids in file_requirement_ids.items()
        },
    }
    result = calculate_exact_coverage(
        original_requirement_ids,
        coverage_status_by_id,
        commitments,
    )
    unmapped_stages = tuple(
        stage for stage, requirement_ids in stage_requirement_ids.items()
        if not requirement_ids
    )
    unmapped_files = tuple(
        path for path, requirement_ids in file_requirement_ids.items()
        if not requirement_ids
    )
    return result, unmapped_stages, unmapped_files
