"""Operational dossier models for human-centered service-order rounds.

The first foundation of the Mais Humana platform already produced matrixes,
reports, charts, orders, and semantic storage.  This module adds a second layer:
compact operational records that explain why a service order exists, which
blockers are real, what is merely a planned dependency, and how a human should
understand the status without reading each source repository.

The models are intentionally explicit.  They are used by the report generator,
the service-order justifier, the central closeout writer, and tests.  Keeping
the structure here avoids spreading status strings across rendering code.
"""

from __future__ import annotations

import hashlib
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, Iterable, Mapping, Sequence

from .models import Evidence, NeedCategory, OrderType, PlatformHumanReport, Recommendation, ServiceOrder, as_plain_data, merge_unique, slugify, utc_now


class SignalSeverity(str, Enum):
    """How strongly a signal impacts human readiness."""

    INFO = "info"
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


class SignalKind(str, Enum):
    """Normalized classes of operational findings."""

    CAPABILITY = "capability"
    BLOCKER = "blocker"
    RISK = "risk"
    EXCEPTION = "exception"
    DECISION = "decision"
    GAP = "gap"
    EVIDENCE = "evidence"


class SourceConfidence(str, Enum):
    """Confidence of a source after local analysis."""

    DIRECT = "direct"
    DERIVED = "derived"
    DECLARED = "declared"
    INFERRED = "inferred"
    MISSING = "missing"


class HumanReadinessStage(str, Enum):
    """Lifecycle stage from a human-operational point of view."""

    NOT_FOUND = "not_found"
    LOCAL_ONLY = "local_only"
    CATALOG_ONLY = "catalog_only"
    PLANNED = "planned"
    TECHNICAL_READY = "technical_ready"
    HUMAN_EXPLAINABLE = "human_explainable"
    PANEL_READY = "panel_ready"
    CONTROLLED_READY = "controlled_ready"
    PRODUCTION_READY = "production_ready"
    BLOCKED = "blocked"


class GateOutcome(str, Enum):
    """Outcome of a readiness gate."""

    PASS = "pass"
    ATTENTION = "attention"
    FAIL = "fail"
    BLOCKED = "blocked"
    NOT_APPLICABLE = "not_applicable"


class GateDomain(str, Enum):
    """Gate domains aligned to ecosystem governance."""

    REPOSITORY = "repository"
    DOCUMENTATION = "documentation"
    RUNTIME = "runtime"
    TESTS = "tests"
    CONTRACT = "contract"
    PANEL = "panel"
    IDENTITY = "identity"
    BUSINESS = "business"
    COMPLIANCE = "compliance"
    INTEGRATION = "integration"
    DOCS = "docs"
    OBSERVABILITY = "observability"
    SECURITY = "security"
    GOVERNANCE = "governance"
    CLOUD = "cloud"


class OrderClosureStatus(str, Enum):
    """Status used when an active order is treated in a round."""

    COMPLETED = "concluida"
    PARTIAL = "parcial"
    BLOCKED = "bloqueada"
    SUPERSEDED = "substituida"


class EvidenceRole(str, Enum):
    """Role of a piece of evidence in a human explanation."""

    PRIMARY = "primary"
    SUPPORTING = "supporting"
    CONTRADICTORY = "contradictory"
    ABSENT = "absent"


STAGE_ORDER: dict[HumanReadinessStage, int] = {
    HumanReadinessStage.NOT_FOUND: 0,
    HumanReadinessStage.BLOCKED: 1,
    HumanReadinessStage.PLANNED: 2,
    HumanReadinessStage.CATALOG_ONLY: 3,
    HumanReadinessStage.LOCAL_ONLY: 4,
    HumanReadinessStage.TECHNICAL_READY: 5,
    HumanReadinessStage.HUMAN_EXPLAINABLE: 6,
    HumanReadinessStage.PANEL_READY: 7,
    HumanReadinessStage.CONTROLLED_READY: 8,
    HumanReadinessStage.PRODUCTION_READY: 9,
}


SEVERITY_WEIGHT: dict[SignalSeverity, int] = {
    SignalSeverity.INFO: 0,
    SignalSeverity.LOW: 1,
    SignalSeverity.MEDIUM: 3,
    SignalSeverity.HIGH: 6,
    SignalSeverity.CRITICAL: 10,
}


def stable_digest(value: object, length: int = 16) -> str:
    """Return a deterministic digest for compact evidence IDs."""

    encoded = repr(as_plain_data(value)).encode("utf-8", errors="ignore")
    return hashlib.sha256(encoded).hexdigest()[:length]


def severity_rank(value: SignalSeverity) -> int:
    return SEVERITY_WEIGHT.get(value, 0)


def stage_rank(value: HumanReadinessStage) -> int:
    return STAGE_ORDER.get(value, 0)


def worst_severity(values: Iterable[SignalSeverity]) -> SignalSeverity:
    ordered = sorted(values, key=severity_rank, reverse=True)
    return ordered[0] if ordered else SignalSeverity.INFO


def highest_stage(values: Iterable[HumanReadinessStage]) -> HumanReadinessStage:
    ordered = sorted(values, key=stage_rank, reverse=True)
    return ordered[0] if ordered else HumanReadinessStage.NOT_FOUND


def lowest_stage(values: Iterable[HumanReadinessStage]) -> HumanReadinessStage:
    ordered = sorted(values, key=stage_rank)
    return ordered[0] if ordered else HumanReadinessStage.NOT_FOUND


def normalize_reference(path: str | Path, line: int | None = None) -> str:
    ref = str(path).replace("\\", "/")
    if line is not None:
        return f"{ref}:{line}"
    return ref


@dataclass(slots=True)
class SourceReference:
    """Local source used to justify a signal."""

    path: str
    summary: str
    line: int | None = None
    confidence: SourceConfidence = SourceConfidence.INFERRED
    role: EvidenceRole = EvidenceRole.SUPPORTING

    @property
    def reference(self) -> str:
        return normalize_reference(self.path, self.line)

    def to_dict(self) -> dict[str, Any]:
        return as_plain_data(self)

    @classmethod
    def from_evidence(cls, evidence: Evidence, *, role: EvidenceRole = EvidenceRole.SUPPORTING) -> "SourceReference":
        confidence = SourceConfidence.DIRECT if evidence.confidence >= 0.75 else SourceConfidence.DERIVED
        return cls(
            path=evidence.path,
            line=evidence.line,
            summary=evidence.summary,
            confidence=confidence,
            role=role,
        )


@dataclass(slots=True)
class OperationalSignal:
    """A normalized signal detected from code, docs, warnings, or catalog data."""

    signal_id: str
    platform_id: str
    kind: SignalKind
    domain: GateDomain
    title: str
    summary: str
    severity: SignalSeverity
    stage: HumanReadinessStage
    categories: tuple[NeedCategory, ...] = ()
    sources: tuple[SourceReference, ...] = ()
    tags: tuple[str, ...] = ()
    next_action: str = "registrar continuidade operacional"
    created_at: str = field(default_factory=utc_now)

    def to_dict(self) -> dict[str, Any]:
        return as_plain_data(self)

    @property
    def is_blocking(self) -> bool:
        return self.kind == SignalKind.BLOCKER or self.severity in {SignalSeverity.HIGH, SignalSeverity.CRITICAL}

    @property
    def reference_digest(self) -> str:
        return stable_digest((self.signal_id, self.sources, self.summary), length=12)

    def compact_line(self) -> str:
        return f"{self.platform_id}:{self.domain.value}:{self.kind.value}:{self.severity.value}:{self.title}"


@dataclass(slots=True)
class ReadinessGate:
    """One human-readiness gate for a platform."""

    gate_id: str
    platform_id: str
    domain: GateDomain
    title: str
    outcome: GateOutcome
    severity: SignalSeverity
    reason: str
    next_action: str
    evidence: tuple[SourceReference, ...] = ()
    linked_signals: tuple[str, ...] = ()

    def to_dict(self) -> dict[str, Any]:
        return as_plain_data(self)

    @property
    def passed(self) -> bool:
        return self.outcome == GateOutcome.PASS

    @property
    def blocks_panel(self) -> bool:
        return self.outcome in {GateOutcome.FAIL, GateOutcome.BLOCKED} and self.severity in {SignalSeverity.HIGH, SignalSeverity.CRITICAL}

    def compact_line(self) -> str:
        return f"{self.gate_id}={self.outcome.value}/{self.severity.value}"


@dataclass(slots=True)
class PlatformOperationalDossier:
    """Human-operational dossier for one platform."""

    platform_id: str
    title: str
    repo_path: str
    stage: HumanReadinessStage
    human_score: int
    technical_ready: bool
    panel_ready: bool
    same_source_ready: bool
    blocker_count: int
    warning_count: int
    gates: tuple[ReadinessGate, ...]
    signals: tuple[OperationalSignal, ...]
    top_next_actions: tuple[str, ...]
    order_targets: tuple[str, ...]
    generated_at: str = field(default_factory=utc_now)

    def to_dict(self) -> dict[str, Any]:
        return as_plain_data(self)

    @property
    def critical_signals(self) -> tuple[OperationalSignal, ...]:
        return tuple(signal for signal in self.signals if signal.severity == SignalSeverity.CRITICAL)

    @property
    def blocking_signals(self) -> tuple[OperationalSignal, ...]:
        return tuple(signal for signal in self.signals if signal.is_blocking)

    @property
    def status_label(self) -> str:
        if self.critical_signals:
            return "critico"
        if self.blocker_count:
            return "bloqueado"
        if self.panel_ready and self.same_source_ready:
            return "pronto-para-painel"
        if self.technical_ready:
            return "tecnico-pronto"
        return "atencao"

    @property
    def primary_action(self) -> str:
        if self.top_next_actions:
            return self.top_next_actions[0]
        if self.blocker_count:
            return "resolver bloqueios antes de promover prontidao"
        if not self.panel_ready:
            return "materializar contrato de painel e mesma fonte"
        return "manter evidencias e regressao"

    def signals_by_domain(self) -> dict[str, list[OperationalSignal]]:
        grouped: dict[str, list[OperationalSignal]] = {}
        for signal in self.signals:
            grouped.setdefault(signal.domain.value, []).append(signal)
        for items in grouped.values():
            items.sort(key=lambda item: (-severity_rank(item.severity), item.title))
        return grouped


@dataclass(slots=True)
class OrderJustification:
    """Why a service order exists and what evidence supports it."""

    order_id: str
    order_type: OrderType
    platform_id: str
    title: str
    closure_status: OrderClosureStatus
    reason: str
    execution_summary: str
    evidence: tuple[SourceReference, ...]
    linked_signals: tuple[str, ...]
    linked_gates: tuple[str, ...]
    resulting_orders: tuple[str, ...]
    pending_items: tuple[str, ...]
    validation_steps: tuple[str, ...]
    generated_at: str = field(default_factory=utc_now)

    def to_dict(self) -> dict[str, Any]:
        return as_plain_data(self)

    @property
    def has_pending(self) -> bool:
        return bool(self.pending_items)

    @property
    def compact_status(self) -> str:
        pending = "com-pendencia" if self.has_pending else "sem-pendencia"
        return f"{self.closure_status.value}:{pending}"


@dataclass(slots=True)
class ExecutionRoundDossier:
    """Full dossier for the current service-order round."""

    round_id: str
    project_id: str
    generated_at: str
    platform_dossiers: tuple[PlatformOperationalDossier, ...]
    order_justifications: tuple[OrderJustification, ...]
    active_input_orders: tuple[str, ...]
    output_orders: tuple[str, ...]
    executive_summary: tuple[str, ...]
    managerial_summary: tuple[str, ...]
    pending_items: tuple[str, ...]
    total_code_lines_analyzed: int
    code_lines_available_in_project: int

    def to_dict(self) -> dict[str, Any]:
        return as_plain_data(self)

    @property
    def blockers(self) -> tuple[OperationalSignal, ...]:
        signals: list[OperationalSignal] = []
        for dossier in self.platform_dossiers:
            signals.extend(dossier.blocking_signals)
        signals.sort(key=lambda item: (-severity_rank(item.severity), item.platform_id, item.title))
        return tuple(signals)

    @property
    def completed_orders(self) -> tuple[OrderJustification, ...]:
        return tuple(item for item in self.order_justifications if item.closure_status == OrderClosureStatus.COMPLETED)

    @property
    def partial_orders(self) -> tuple[OrderJustification, ...]:
        return tuple(item for item in self.order_justifications if item.closure_status == OrderClosureStatus.PARTIAL)

    @property
    def blocked_orders(self) -> tuple[OrderJustification, ...]:
        return tuple(item for item in self.order_justifications if item.closure_status == OrderClosureStatus.BLOCKED)

    def dossier_for(self, platform_id: str) -> PlatformOperationalDossier | None:
        for dossier in self.platform_dossiers:
            if dossier.platform_id == platform_id:
                return dossier
        return None


def source_refs_from_strings(values: Iterable[str], summary: str, confidence: SourceConfidence = SourceConfidence.DECLARED) -> tuple[SourceReference, ...]:
    refs: list[SourceReference] = []
    for value in values:
        refs.append(SourceReference(path=str(value), summary=summary, confidence=confidence))
    return tuple(refs)


def source_refs_from_evidence(values: Iterable[Evidence], limit: int = 6) -> tuple[SourceReference, ...]:
    refs: list[SourceReference] = []
    for evidence in values:
        refs.append(SourceReference.from_evidence(evidence))
        if len(refs) >= limit:
            break
    return tuple(refs)


def summarize_signal_titles(signals: Sequence[OperationalSignal], limit: int = 8) -> tuple[str, ...]:
    ordered = sorted(signals, key=lambda item: (-severity_rank(item.severity), item.platform_id, item.title))
    return merge_unique(f"{signal.platform_id}: {signal.title}" for signal in ordered[:limit])


def summarize_gate_failures(gates: Sequence[ReadinessGate], limit: int = 8) -> tuple[str, ...]:
    ordered = sorted(
        [gate for gate in gates if not gate.passed],
        key=lambda item: (-severity_rank(item.severity), item.platform_id, item.gate_id),
    )
    return merge_unique(f"{gate.platform_id}: {gate.title} - {gate.next_action}" for gate in ordered[:limit])


def next_actions_from_signals(signals: Sequence[OperationalSignal], gates: Sequence[ReadinessGate], limit: int = 7) -> tuple[str, ...]:
    actions: list[str] = []
    ordered_signals = sorted(signals, key=lambda item: (-severity_rank(item.severity), item.title))
    for signal in ordered_signals:
        actions.append(signal.next_action)
    for gate in sorted(gates, key=lambda item: (-severity_rank(item.severity), item.title)):
        if not gate.passed:
            actions.append(gate.next_action)
    return merge_unique(actions)[:limit]


def order_id_for_recommendation(recommendation: Recommendation, order_type: OrderType, index: int) -> str:
    prefix = "EXECUTIVA" if order_type == OrderType.EXECUTIVE else "GERENCIAL"
    return f"{index:04d}_{prefix}__{slugify(recommendation.title)}"


def compact_report_identity(report: PlatformHumanReport) -> Mapping[str, Any]:
    return {
        "platform_id": report.platform.platform_id,
        "title": report.platform.title,
        "repo_path": report.scan.repo_path,
        "score": report.average_score,
        "warnings": report.scan.warnings,
        "evidence": len(report.scan.evidence),
        "code_lines": report.scan.code_lines,
    }


def find_service_order_platform(order: ServiceOrder | OrderJustification | str, default: str = "ecosystem") -> str:
    if isinstance(order, ServiceOrder):
        text = f"{order.order_id} {order.title} {order.object_scope}".lower()
    elif isinstance(order, OrderJustification):
        text = f"{order.order_id} {order.title} {order.reason}".lower()
    else:
        text = str(order).lower()
    known = (
        "business",
        "compliance",
        "customer_ops",
        "docs",
        "finance",
        "gettys",
        "identity",
        "integracoes",
        "intelligence",
        "mcps",
        "platform_base",
        "public",
        "stj",
        "ui",
    )
    for platform_id in known:
        if platform_id in text or platform_id.replace("_", "-") in text:
            return platform_id
    return default


def code_line_count(root: Path) -> int:
    """Count local project code lines for production-minimum reporting."""

    extensions = {".py", ".ts", ".tsx", ".js", ".mjs", ".cjs", ".java"}
    skipped = {"node_modules", ".git", "dist", "build", "coverage", "__pycache__", ".pytest_cache"}
    total = 0
    if not root.exists():
        return 0
    stack = [root]
    while stack:
        current = stack.pop()
        try:
            entries = list(current.iterdir())
        except OSError:
            continue
        for entry in entries:
            if entry.is_dir():
                if entry.name not in skipped:
                    stack.append(entry)
                continue
            if entry.suffix.lower() not in extensions:
                continue
            try:
                with entry.open("r", encoding="utf-8", errors="ignore") as handle:
                    total += sum(1 for _ in handle)
            except OSError:
                continue
    return total
