"""Core models for human-centered ecosystem analysis.

The module intentionally keeps the data model dependency-free.  The platform is
expected to run inside operational mirrors where installing new packages is not
always desirable, so every object can be serialized with the standard library.
"""

from __future__ import annotations

from dataclasses import dataclass, field, fields, is_dataclass
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Any, Iterable, Mapping, MutableMapping, Sequence


def utc_now() -> str:
    """Return an ISO-8601 timestamp without relying on local locale settings."""

    return datetime.now(timezone.utc).replace(microsecond=0).isoformat()


def slugify(value: str) -> str:
    """Create a stable ASCII-ish slug for filenames and object ids."""

    allowed: list[str] = []
    last_dash = False
    for char in value.lower().strip():
        if "a" <= char <= "z" or "0" <= char <= "9":
            allowed.append(char)
            last_dash = False
        elif char in {" ", "_", "-", ".", "/", "\\"} and not last_dash:
            allowed.append("-")
            last_dash = True
    return "".join(allowed).strip("-") or "item"


def as_plain_data(value: Any) -> Any:
    """Convert dataclasses, enums, paths, and nested values to JSON-safe data."""

    if isinstance(value, Enum):
        return value.value
    if isinstance(value, Path):
        return str(value)
    if is_dataclass(value):
        return {item.name: as_plain_data(getattr(value, item.name)) for item in fields(value)}
    if isinstance(value, Mapping):
        return {str(key): as_plain_data(inner) for key, inner in value.items()}
    if isinstance(value, (list, tuple, set)):
        return [as_plain_data(item) for item in value]
    return value


class EvidenceKind(str, Enum):
    """Kinds of evidence detected in a platform repository."""

    README = "readme"
    PACKAGE_SCRIPT = "package_script"
    ROUTE = "route"
    OPENAPI = "openapi"
    TEST = "test"
    CONFIG = "config"
    DOC = "doc"
    WORKER = "worker"
    STORAGE = "storage"
    MCP_TOOL = "mcp_tool"
    UI_SURFACE = "ui_surface"
    SECURITY = "security"
    BUSINESS_RULE = "business_rule"
    OBSERVABILITY = "observability"
    UNKNOWN = "unknown"


class NeedCategory(str, Enum):
    """Human need categories used by the matrix engine."""

    ADMINISTRATION = "administration"
    SUPPORT = "support"
    FINANCE = "finance"
    LEGAL = "legal"
    SECURITY = "security"
    OPERATIONS = "operations"
    STRATEGY = "strategy"
    DOCUMENTATION = "documentation"
    SELF_SERVICE = "self_service"
    COMMERCIAL = "commercial"
    EXPERIENCE = "experience"
    GOVERNANCE = "governance"
    INTEGRATION = "integration"
    OBSERVABILITY = "observability"


class MaturityLevel(str, Enum):
    """Operational maturity level from a human point of view."""

    NOT_FOUND = "not_found"
    PLANNED = "planned"
    CATALOGED = "cataloged"
    TECHNICAL = "technical"
    EXPLAINABLE = "explainable"
    ACTIONABLE = "actionable"
    READY_FOR_HUMAN = "ready_for_human"
    AUDITABLE = "auditable"


class OrderType(str, Enum):
    """Service-order type used by the order generator."""

    EXECUTIVE = "executiva"
    MANAGERIAL = "gerencial"


class OrderStatus(str, Enum):
    """Compact status for generated and executed orders."""

    PLANNED = "planejada"
    RUNNING = "em_execucao"
    COMPLETED = "concluida"
    PARTIAL = "parcial"
    BLOCKED = "bloqueada"


@dataclass(slots=True)
class Evidence:
    """A small, inspectable proof that a capability exists or is missing."""

    kind: EvidenceKind
    path: str
    summary: str
    line: int | None = None
    confidence: float = 0.5
    tags: tuple[str, ...] = ()

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

    @property
    def reference(self) -> str:
        if self.line is None:
            return self.path
        return f"{self.path}:{self.line}"

    def is_strong(self) -> bool:
        return self.confidence >= 0.75


@dataclass(slots=True)
class HumanNeed:
    """A concrete need a person may have while using the ecosystem."""

    need_id: str
    title: str
    category: NeedCategory
    description: str
    success_markers: tuple[str, ...]
    risk_if_missing: str
    expected_surfaces: tuple[str, ...] = ()

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


@dataclass(slots=True)
class HumanProfile:
    """A human role that should be served by one or more platforms."""

    profile_id: str
    name: str
    description: str
    priority_needs: tuple[NeedCategory, ...]
    typical_questions: tuple[str, ...]
    expected_outputs: tuple[str, ...]
    sensitive_concerns: tuple[str, ...] = ()

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

    def wants(self, category: NeedCategory) -> bool:
        return category in self.priority_needs


@dataclass(slots=True)
class PlatformDefinition:
    """Canonical description of a managed platform."""

    platform_id: str
    repo_name: str
    central_folder: str
    title: str
    mission: str
    primary_categories: tuple[NeedCategory, ...]
    expected_profiles: tuple[str, ...]
    related_platforms: tuple[str, ...] = ()
    expected_surfaces: tuple[str, ...] = ()
    known_blockers: tuple[str, ...] = ()

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


@dataclass(slots=True)
class FileMetric:
    """Line and extension metric for a repository file."""

    path: str
    extension: str
    lines: int
    bytes_size: int

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


@dataclass(slots=True)
class ScriptCommand:
    """A command exposed by package.json or other local metadata."""

    name: str
    command: str
    source_file: str
    intent: str = "unknown"

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


@dataclass(slots=True)
class PlatformScan:
    """Repository scan result used as input for matrix and report generation."""

    platform: PlatformDefinition
    repo_path: str
    exists: bool
    git_present: bool
    branch: str | None
    head: str | None
    remote_origin: str | None
    readme_excerpt: str
    file_metrics: tuple[FileMetric, ...]
    scripts: tuple[ScriptCommand, ...]
    evidence: tuple[Evidence, ...]
    warnings: tuple[str, ...]
    scanned_at: str = field(default_factory=utc_now)

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

    @property
    def total_lines(self) -> int:
        return sum(metric.lines for metric in self.file_metrics)

    @property
    def code_lines(self) -> int:
        code_ext = {".ts", ".tsx", ".js", ".mjs", ".cjs", ".py", ".java"}
        return sum(metric.lines for metric in self.file_metrics if metric.extension in code_ext)

    @property
    def has_tests(self) -> bool:
        return any(item.kind == EvidenceKind.TEST for item in self.evidence)

    @property
    def has_openapi(self) -> bool:
        return any(item.kind == EvidenceKind.OPENAPI for item in self.evidence)

    @property
    def has_worker(self) -> bool:
        return any(item.kind == EvidenceKind.WORKER for item in self.evidence)

    @property
    def strong_evidence_count(self) -> int:
        return sum(1 for item in self.evidence if item.is_strong())

    def evidence_by_kind(self, kind: EvidenceKind) -> tuple[Evidence, ...]:
        return tuple(item for item in self.evidence if item.kind == kind)


@dataclass(slots=True)
class MatrixCell:
    """Human service score for one platform and one profile."""

    platform_id: str
    profile_id: str
    score: int
    maturity: MaturityLevel
    explanation: str
    strengths: tuple[str, ...]
    gaps: tuple[str, ...]
    evidence_refs: tuple[str, ...]

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

    @property
    def normalized_score(self) -> float:
        return max(0.0, min(1.0, self.score / 100.0))

    def is_ready(self) -> bool:
        return self.score >= 75


@dataclass(slots=True)
class Recommendation:
    """Actionable recommendation derived from scan and matrix signals."""

    recommendation_id: str
    platform_id: str
    title: str
    reason: str
    expected_impact: str
    categories: tuple[NeedCategory, ...]
    priority: int
    suggested_order_type: OrderType
    affected_paths: tuple[str, ...] = ()
    validation_steps: tuple[str, ...] = ()

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


@dataclass(slots=True)
class PlatformHumanReport:
    """Report model for one platform."""

    platform: PlatformDefinition
    scan: PlatformScan
    cells: tuple[MatrixCell, ...]
    recommendations: tuple[Recommendation, ...]
    summary: str
    current_state: tuple[str, ...]
    future_state: tuple[str, ...]
    missing_for_humans: tuple[str, ...]
    generated_at: str = field(default_factory=utc_now)

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

    @property
    def average_score(self) -> int:
        if not self.cells:
            return 0
        return round(sum(cell.score for cell in self.cells) / len(self.cells))

    @property
    def ready_profiles(self) -> tuple[str, ...]:
        return tuple(cell.profile_id for cell in self.cells if cell.is_ready())


@dataclass(slots=True)
class EcosystemHumanReport:
    """Full ecosystem report model."""

    scans: tuple[PlatformScan, ...]
    platform_reports: tuple[PlatformHumanReport, ...]
    recommendations: tuple[Recommendation, ...]
    generated_at: str = field(default_factory=utc_now)

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

    @property
    def total_code_lines(self) -> int:
        return sum(scan.code_lines for scan in self.scans)

    @property
    def total_files(self) -> int:
        return sum(len(scan.file_metrics) for scan in self.scans)

    @property
    def average_score(self) -> int:
        cells = [cell for report in self.platform_reports for cell in report.cells]
        if not cells:
            return 0
        return round(sum(cell.score for cell in cells) / len(cells))

    def report_for(self, platform_id: str) -> PlatformHumanReport | None:
        for report in self.platform_reports:
            if report.platform.platform_id == platform_id:
                return report
        return None


@dataclass(slots=True)
class ServiceOrder:
    """Generated service order metadata and body."""

    order_id: str
    order_type: OrderType
    project_id: str
    title: str
    purpose: str
    object_scope: str
    reason: str
    expected_result: str
    affected_paths: tuple[str, ...]
    validations: tuple[str, ...]
    ready_criteria: tuple[str, ...]
    status: OrderStatus = OrderStatus.PLANNED
    priority: str = "media"

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


@dataclass(slots=True)
class GeneratedFile:
    """File generated or updated by the platform runtime."""

    path: str
    description: str
    function: str
    file_type: str
    changed_by: str
    change_summary: str
    relation_to_order: str

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


@dataclass(slots=True)
class ReportBundle:
    """Paths and counters produced by a generation run."""

    output_root: str
    generated_files: tuple[GeneratedFile, ...]
    platform_count: int
    profile_count: int
    matrix_cells: int
    total_code_lines_analyzed: int
    warnings: tuple[str, ...]
    generated_at: str = field(default_factory=utc_now)

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


def clamp_score(value: int | float) -> int:
    """Clamp a numeric score into the 0..100 range."""

    return int(max(0, min(100, round(float(value)))))


def maturity_from_score(score: int) -> MaturityLevel:
    """Map numeric score to a human maturity label."""

    score = clamp_score(score)
    if score == 0:
        return MaturityLevel.NOT_FOUND
    if score < 20:
        return MaturityLevel.PLANNED
    if score < 35:
        return MaturityLevel.CATALOGED
    if score < 50:
        return MaturityLevel.TECHNICAL
    if score < 65:
        return MaturityLevel.EXPLAINABLE
    if score < 80:
        return MaturityLevel.ACTIONABLE
    if score < 92:
        return MaturityLevel.READY_FOR_HUMAN
    return MaturityLevel.AUDITABLE


def merge_unique(values: Iterable[str]) -> tuple[str, ...]:
    """Return values in input order while dropping empty strings and duplicates."""

    seen: set[str] = set()
    output: list[str] = []
    for value in values:
        cleaned = str(value).strip()
        if not cleaned or cleaned in seen:
            continue
        seen.add(cleaned)
        output.append(cleaned)
    return tuple(output)


def group_by_platform(recommendations: Sequence[Recommendation]) -> dict[str, list[Recommendation]]:
    grouped: dict[str, list[Recommendation]] = {}
    for item in recommendations:
        grouped.setdefault(item.platform_id, []).append(item)
    for items in grouped.values():
        items.sort(key=lambda rec: (-rec.priority, rec.title))
    return grouped


def incrementing_id(prefix: str, index: int, title: str) -> str:
    """Create a readable id with a numeric prefix and a slug."""

    return f"{index:04d}_{prefix}__{slugify(title)}"


def summarize_warnings(scans: Sequence[PlatformScan]) -> tuple[str, ...]:
    warnings: list[str] = []
    for scan in scans:
        for warning in scan.warnings:
            warnings.append(f"{scan.platform.platform_id}: {warning}")
    return merge_unique(warnings)


def score_label(score: int) -> str:
    """Return a compact label for user-facing matrix tables."""

    score = clamp_score(score)
    if score >= 90:
        return "excelente"
    if score >= 75:
        return "forte"
    if score >= 60:
        return "util"
    if score >= 40:
        return "tecnico"
    if score >= 20:
        return "inicial"
    if score > 0:
        return "fragil"
    return "ausente"


def ensure_mapping(data: MutableMapping[str, Any], key: str, default: Any) -> Any:
    """Small helper used by storage migration code."""

    if key not in data:
        data[key] = default
    return data[key]

