"""Regulatory Optimization — the Roadmap / Management RENDERER of the Capability Delta Engine.

GAP analysis and measure-prioritisation are TWO VIEWS OF THE SAME COMPUTATION. The Capability
Delta Engine (`compliance/transition_reasoning`, RS-005) computes Required - Known = the
Capability Delta once. Renderers read that ONE delta:
  - Interview Renderer  (missing INFORMATION -> questions)    = `TransitionQuestionRequest` (built)
  - Roadmap / Management Renderer (missing CAPABILITIES -> measures by leverage) = THIS module
  - Evidence Renderer   (missing EVIDENCE -> upload requests) = later
There is one truth, not a Gap engine and a separate Roadmap engine.

A measure (a capability to implement) has *regulatory leverage* = the number of distinct
regulatory requirements it closes AT ONCE (e.g. patch management closes a CRA, a MaschinenVO,
an IEC 62443 and an ISO 27001 requirement -> leverage 4). The product turns from "you have N
obligations" into "of N identified requirements you only need M measures — and these K first".

Fully deterministic, computed-not-stored, NO new corpus. `regulatory_leverage`/`select_within_budget`
are pure math over `capability -> requirements`; `roadmap_from_delta` binds them to the RS-005
delta (dependency optimization -> transition_reasoning, acyclic; the delta engine stays hermetic).
No new graph/meta-model class (freeze v1.0). Python 3.9 compatible.

Honesty (Welt-1): the percentages are exact count ratios over the IDENTIFIED requirements from
the known patterns — never "% gesetzeskonform". Label outputs as "der identifizierten Anforderungen".
"""

from __future__ import annotations

from typing import Dict, List, Optional

from ..transition_reasoning import CoverageStatus, TransitionAssessment
from .schemas import BudgetPlan, OptimizationPlan, RankedMeasure


def _ranked(
    capability_requirements: Dict[str, List[str]], in_scope: Optional[List[str]]
) -> List[RankedMeasure]:
    """Rank measures: leverage desc, then capability_id asc (deterministic). Empty covers dropped."""
    scope = (
        set(in_scope)
        if in_scope is not None
        else {r for reqs in capability_requirements.values() for r in reqs}
    )
    measures: List[RankedMeasure] = []
    for cap, reqs in capability_requirements.items():
        covers = sorted({r for r in reqs if r in scope})
        if not covers:
            continue  # this capability closes nothing in scope -> not a measure here
        measures.append(RankedMeasure(capability_id=cap, covers=covers, leverage=len(covers)))
    measures.sort(key=lambda m: (-m.leverage, m.capability_id))
    total = sum(m.leverage for m in measures)
    running = 0
    for m in measures:
        running += m.leverage
        m.cumulative_requirements = running
        m.cumulative_coverage = (running / total) if total else 0.0
    return measures


def regulatory_leverage(
    capability_requirements: Dict[str, List[str]], in_scope: Optional[List[str]] = None
) -> OptimizationPlan:
    """Rank measures by regulatory leverage; report the compression (requirements -> measures).

    `capability_requirements`: measure (capability_id) -> the requirement keys it satisfies. A
    requirement key is currently a regulation (via `covers_targets`); finer obligation granularity
    is a future extension. `in_scope`: restrict the requirement keys counted (default: all seen).
    """
    measures = _ranked(capability_requirements, in_scope)
    scope = sorted(
        set(in_scope)
        if in_scope is not None
        else {r for reqs in capability_requirements.values() for r in reqs}
    )
    total = sum(m.leverage for m in measures)
    avg = (total / len(measures)) if measures else 0.0
    headline = (
        "%d identifizierte Anforderungen aus %d Regelwerken -> %d Massnahmen (Ø Hebel %.1f)."
        % (total, len(scope), len(measures), avg)
    )
    return OptimizationPlan(
        in_scope_requirements=scope,
        total_measures=len(measures),
        total_requirements=total,
        ranked_measures=measures,
        headline=headline,
    )


def select_within_budget(
    capability_requirements: Dict[str, List[str]],
    budget: int,
    in_scope: Optional[List[str]] = None,
) -> BudgetPlan:
    """The budget answer: with K measures, pick the K highest-leverage ones and report coverage.

    Because each requirement key is closed by exactly one measure here, greedy-by-leverage is the
    optimal cover, so ranking == selection. (When requirements become shared across capabilities,
    this becomes weighted set-cover; the signature is ready for that.)
    """
    measures = _ranked(capability_requirements, in_scope)
    total = sum(m.leverage for m in measures)
    k = max(0, budget)
    selected = measures[:k]
    closed = selected[-1].cumulative_requirements if selected else 0
    ratio = (closed / total) if total else 0.0
    headline = (
        "Mit den Top-%d Massnahmen (nach regulatorischem Hebel) schliessen Sie %d von %d "
        "identifizierten Anforderungen (%.0f%%)." % (len(selected), closed, total, ratio * 100)
    )
    return BudgetPlan(
        budget=budget,
        selected_capabilities=[m.capability_id for m in selected],
        requirements_closed=closed,
        total_requirements=total,
        coverage_ratio=ratio,
        headline=headline,
    )


def roadmap_from_delta(
    assessment: TransitionAssessment,
    capability_requirements: Dict[str, List[str]],
    in_scope: Optional[List[str]] = None,
    open_statuses: Optional[List[CoverageStatus]] = None,
) -> OptimizationPlan:
    """Render the Roadmap view FROM a Capability Delta (an RS-005 `TransitionAssessment`).

    Takes the OPEN capabilities of the delta — MISSING by default — and ranks them by regulatory
    leverage. This is the same delta the Interview Renderer turns into questions; here it becomes
    prioritised measures. The binding that makes "one truth, two renderers" real in code.
    """
    statuses = set(open_statuses) if open_statuses is not None else {CoverageStatus.MISSING}
    open_caps = [c.capability_id for c in assessment.coverage if c.status in statuses]
    delta_reqs = {cap: capability_requirements.get(cap, []) for cap in open_caps}
    return regulatory_leverage(delta_reqs, in_scope)
