from __future__ import annotations

from dataclasses import dataclass, field

from app.modules.rag.indexing.code.edges.dataflow_slice_builder import DataflowSlice
from app.modules.rag.indexing.code.edges.extractor import PyEdge
from app.modules.rag.indexing.code.entrypoints.execution_trace_builder import ExecutionTrace
from app.modules.rag.indexing.code.roles.models import SemanticRoleRecord
from app.modules.rag.indexing.code.symbols.extractor import PySymbol

_ROLE_PRIORITY = [
    "handler",
    "service",
    "repository",
    "adapter",
    "parser",
    "pipeline_stage",
    "controller",
    "manager",
    "factory",
    "model",
]
_SUFFIX_TO_ROLE = {
    "handler": "handler",
    "service": "service",
    "repository": "repository",
    "parser": "parser",
    "controller": "controller",
    "adapter": "adapter",
    "manager": "manager",
    "factory": "factory",
}
_MODEL_SUFFIXES = ("model", "entity", "schema", "dto")
_PIPELINE_HINTS = ("processor", "stage", "pipeline")
_ADAPTER_MODULES = {"requests", "httpx", "aiohttp", "smtplib", "sqlalchemy"}
_PARSER_MODULES = {"openpyxl", "csv", "xlrd", "pandas"}
_IGNORE_NAMES = {"logger", "logging", "typing", "dataclass"}


@dataclass(slots=True)
class _RoleScore:
    values: dict[str, float] = field(default_factory=lambda: {role: 0.0 for role in _ROLE_PRIORITY})
    reasons: list[str] = field(default_factory=list)

    def add(self, role: str, score: float, reason: str) -> None:
        self.values[role] = self.values.get(role, 0.0) + score
        if reason and reason not in self.reasons:
            self.reasons.append(reason)

    def top(self) -> tuple[str, float]:
        best_role = "model"
        best_value = -1.0
        for role in _ROLE_PRIORITY:
            value = self.values.get(role, 0.0)
            if value > best_value:
                best_role = role
                best_value = value
        return best_role, best_value

    def confidence(self) -> float:
        total = sum(value for value in self.values.values() if value > 0)
        _, top_score = self.top()
        if total <= 0:
            return 0.51
        ratio = top_score / total
        return round(min(0.99, max(0.55, ratio)), 2)


class SemanticRoleScorer:
    def score(
        self,
        *,
        symbol: PySymbol,
        class_edges: list[PyEdge],
        dataflow_slices: list[DataflowSlice],
        execution_traces: list[ExecutionTrace],
    ) -> SemanticRoleRecord | None:
        if symbol.kind != "class":
            return None
        symbol_name = symbol.qname
        class_name = symbol_name.rsplit(".", 1)[-1]
        lowered_name = class_name.lower()
        if any(marker in lowered_name for marker in _IGNORE_NAMES):
            return None
        role_score = _RoleScore()
        self._score_by_name(lowered_name, role_score)
        import_modules = _import_modules(class_edges)
        self._score_by_imports(import_modules, role_score)
        calls_to_roles = _calls_to_role_like_targets(class_edges)
        reads_count = _edge_count(class_edges, "reads_attr")
        writes_count = _edge_count(class_edges, "writes_attr")
        self._score_pipeline_signals(
            lowered_name=lowered_name,
            calls_to_roles=calls_to_roles,
            reads_count=reads_count,
            writes_count=writes_count,
            role_score=role_score,
        )
        dataflow_participation = _dataflow_participation(symbol_name, class_name, dataflow_slices)
        if dataflow_participation > 0:
            role_score.add("pipeline_stage", 2.0, f"participates in dataflow slices ({dataflow_participation})")
        execution_trace_proximity = _trace_proximity(symbol_name, class_name, execution_traces)
        if execution_trace_proximity > 0:
            role_score.add("pipeline_stage", 1.0, f"appears in execution traces ({execution_trace_proximity})")
        role, top_score = role_score.top()
        if top_score <= 0:
            role_score.add("model", 1.0, "default model role")
            role = "model"
        responsibilities = role_score.reasons[:4]
        if not responsibilities:
            responsibilities = ["class-level domain responsibility"]
        return SemanticRoleRecord(
            symbol_id=symbol.symbol_id,
            symbol_name=symbol_name,
            role=role,
            confidence=role_score.confidence(),
            responsibilities=responsibilities,
            dataflow_participation=dataflow_participation,
            execution_trace_proximity=execution_trace_proximity,
            start_line=symbol.start_line,
            end_line=symbol.end_line,
        )

    def _score_by_name(self, lowered_name: str, role_score: _RoleScore) -> None:
        for suffix, role in _SUFFIX_TO_ROLE.items():
            if lowered_name.endswith(suffix):
                role_score.add(role, 3.0, f"name suffix suggests {role}")
        if lowered_name.endswith(_MODEL_SUFFIXES):
            role_score.add("model", 2.0, "name suffix suggests model")
        if any(marker in lowered_name for marker in _PIPELINE_HINTS):
            role_score.add("pipeline_stage", 2.0, "name indicates pipeline orchestration")

    def _score_by_imports(self, import_modules: set[str], role_score: _RoleScore) -> None:
        adapter_hits = import_modules & _ADAPTER_MODULES
        if adapter_hits:
            role_score.add("adapter", min(4.0, 2.0 * len(adapter_hits)), "uses external I/O client modules")
        parser_hits = import_modules & _PARSER_MODULES
        if parser_hits:
            role_score.add("parser", min(4.0, 2.0 * len(parser_hits)), "uses parsing libraries")

    def _score_pipeline_signals(
        self,
        *,
        lowered_name: str,
        calls_to_roles: int,
        reads_count: int,
        writes_count: int,
        role_score: _RoleScore,
    ) -> None:
        if calls_to_roles >= 2:
            role_score.add("pipeline_stage", 2.0, f"orchestrates role-like calls ({calls_to_roles})")
        if reads_count > 0 and writes_count > 0:
            role_score.add("pipeline_stage", 2.0, "reads and writes state attributes")
        if lowered_name.endswith("handler") and (reads_count > 0 or writes_count > 0):
            role_score.add("handler", 1.0, "handler works with pipeline state")


def _import_modules(edges: list[PyEdge]) -> set[str]:
    modules: set[str] = set()
    for edge in edges:
        if edge.edge_type != "imports":
            continue
        dst = str(edge.dst_ref or "")
        if not dst:
            continue
        modules.add(dst.split(".", 1)[0].lower())
    return modules


def _calls_to_role_like_targets(edges: list[PyEdge]) -> int:
    total = 0
    for edge in edges:
        if edge.edge_type not in {"calls", "instantiates"}:
            continue
        target = str(edge.dst_ref or "").rsplit(".", 1)[-1]
        lowered = target.lower()
        if any(lowered.endswith(suffix) for suffix in _SUFFIX_TO_ROLE):
            total += 1
    return total


def _edge_count(edges: list[PyEdge], edge_type: str) -> int:
    return sum(1 for edge in edges if edge.edge_type == edge_type)


def _dataflow_participation(symbol_name: str, class_name: str, slices: list[DataflowSlice]) -> int:
    total = 0
    for item in slices:
        if any(_matches_symbol(value, symbol_name, class_name) for value in item.path_symbols):
            total += 1
    return total


def _trace_proximity(symbol_name: str, class_name: str, traces: list[ExecutionTrace]) -> int:
    total = 0
    for item in traces:
        if any(_matches_symbol(value, symbol_name, class_name) for value in item.path_symbols):
            total += 1
    return total


def _matches_symbol(path_symbol: str, symbol_name: str, class_name: str) -> bool:
    if path_symbol == symbol_name or path_symbol == class_name:
        return True
    return path_symbol.startswith(f"{symbol_name}.") or path_symbol.startswith(f"{class_name}.")
