"""Line-budget and scan-budget helpers for full service-order rounds."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Iterator, Sequence

from .catalog import PLATFORMS
from .models import as_plain_data, merge_unique


CODE_EXTENSIONS = {".py", ".ts", ".tsx", ".js", ".mjs", ".cjs", ".java"}
TECHNICAL_EXTENSIONS = CODE_EXTENSIONS | {".md", ".mdx", ".json", ".toml", ".yml", ".yaml", ".sql"}
SKIP_DIRS = {
    ".git",
    ".test-tmp",
    "node_modules",
    "dist",
    "build",
    "coverage",
    "__pycache__",
    ".pytest_cache",
    ".mypy_cache",
    ".wrangler",
    ".next",
    ".nuxt",
    "vendor",
}


@dataclass(slots=True)
class FileLineBudget:
    path: str
    extension: str
    lines: int
    bytes_size: int
    counted_as_code: bool

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


@dataclass(slots=True)
class RepositoryLineBudget:
    repo_name: str
    repo_path: str
    exists: bool
    files_seen: int
    files_counted: int
    code_lines: int
    technical_lines: int
    largest_files: tuple[FileLineBudget, ...]
    warnings: tuple[str, ...]

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


@dataclass(slots=True)
class RoundLineBudget:
    repositories: tuple[RepositoryLineBudget, ...]
    total_code_lines: int
    total_technical_lines: int
    reading_minimum: int
    production_minimum: int
    reading_minimum_met: bool
    production_minimum_met: bool
    summary: tuple[str, ...]

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

    @property
    def project_budget(self) -> RepositoryLineBudget | None:
        return self.repositories[0] if self.repositories else None

    @property
    def missing_reading_lines(self) -> int:
        return max(0, self.reading_minimum - self.total_technical_lines)

    @property
    def missing_production_lines(self) -> int:
        current = self.project_budget.code_lines if self.project_budget is not None else 0
        return max(0, self.production_minimum - current)


def should_skip(path: Path) -> bool:
    return path.name in SKIP_DIRS


def iter_files(root: Path, max_files: int = 20_000) -> Iterator[Path]:
    if not root.exists():
        return
    stack = [root]
    seen = 0
    while stack and seen < max_files:
        current = stack.pop()
        try:
            entries = sorted(current.iterdir(), key=lambda item: item.name.lower())
        except OSError:
            continue
        for entry in entries:
            if entry.is_dir():
                if not should_skip(entry):
                    stack.append(entry)
                continue
            if entry.is_file():
                seen += 1
                yield entry
                if seen >= max_files:
                    break


def count_lines(path: Path, max_bytes: int = 650_000) -> int:
    try:
        if path.stat().st_size > max_bytes:
            return 0
        with path.open("r", encoding="utf-8", errors="ignore") as handle:
            return sum(1 for _ in handle)
    except OSError:
        return 0


def safe_relative(path: Path, base: Path) -> str:
    try:
        return str(path.relative_to(base)).replace("\\", "/")
    except ValueError:
        return str(path).replace("\\", "/")


def file_budget(path: Path, base: Path, max_bytes: int) -> FileLineBudget | None:
    suffix = path.suffix.lower()
    if suffix not in TECHNICAL_EXTENSIONS:
        return None
    try:
        size = path.stat().st_size
    except OSError:
        return None
    lines = count_lines(path, max_bytes=max_bytes)
    return FileLineBudget(
        path=safe_relative(path, base),
        extension=suffix,
        lines=lines,
        bytes_size=size,
        counted_as_code=suffix in CODE_EXTENSIONS,
    )


def repository_line_budget(
    repo_path: Path,
    repo_name: str | None = None,
    max_files: int = 20_000,
    max_bytes_per_file: int = 650_000,
) -> RepositoryLineBudget:
    repo_name = repo_name or repo_path.name
    if not repo_path.exists():
        return RepositoryLineBudget(
            repo_name=repo_name,
            repo_path=str(repo_path),
            exists=False,
            files_seen=0,
            files_counted=0,
            code_lines=0,
            technical_lines=0,
            largest_files=(),
            warnings=("repositorio nao encontrado",),
        )
    budgets: list[FileLineBudget] = []
    files_seen = 0
    for path in iter_files(repo_path, max_files=max_files):
        files_seen += 1
        item = file_budget(path, repo_path, max_bytes=max_bytes_per_file)
        if item is not None:
            budgets.append(item)
    code_lines = sum(item.lines for item in budgets if item.counted_as_code)
    technical_lines = sum(item.lines for item in budgets)
    warnings: list[str] = []
    if files_seen >= max_files:
        warnings.append(f"limite de arquivos atingido: {max_files}")
    if code_lines == 0:
        warnings.append("nenhuma linha de codigo Python/TS/JS/Java contada")
    largest = tuple(sorted(budgets, key=lambda item: item.lines, reverse=True)[:20])
    return RepositoryLineBudget(
        repo_name=repo_name,
        repo_path=str(repo_path),
        exists=True,
        files_seen=files_seen,
        files_counted=len(budgets),
        code_lines=code_lines,
        technical_lines=technical_lines,
        largest_files=largest,
        warnings=tuple(warnings),
    )


def platform_budget_roots(ecosystem_root: Path) -> tuple[tuple[str, Path], ...]:
    return tuple((platform.repo_name, ecosystem_root / platform.repo_name) for platform in PLATFORMS)


def build_round_line_budget(
    ecosystem_root: Path,
    project_root: Path,
    include_all_platforms: bool = True,
    extra_roots: Sequence[Path] = (),
    reading_minimum: int = 10_000,
    production_minimum: int = 5_500,
) -> RoundLineBudget:
    repo_specs: list[tuple[str, Path]] = []
    repo_specs.append((project_root.name, project_root))
    if include_all_platforms:
        repo_specs.extend(platform_budget_roots(ecosystem_root))
    for root in extra_roots:
        repo_specs.append((root.name, root))
    seen: set[str] = set()
    budgets: list[RepositoryLineBudget] = []
    for name, path in repo_specs:
        key = str(path).lower()
        if key in seen:
            continue
        seen.add(key)
        budgets.append(repository_line_budget(path, name))
    total_code = sum(item.code_lines for item in budgets)
    total_technical = sum(item.technical_lines for item in budgets)
    project_budget = next((item for item in budgets if Path(item.repo_path).resolve() == project_root.resolve()), None)
    production_lines = project_budget.code_lines if project_budget is not None else 0
    summary = (
        f"Repositorios avaliados: {len(budgets)}",
        f"Linhas tecnicas totais: {total_technical}",
        f"Linhas de codigo totais: {total_code}",
        f"Linhas de codigo do projeto real: {production_lines}",
        f"Minimo de leitura cumprido: {total_technical >= reading_minimum}",
        f"Minimo de producao no projeto cumprido: {production_lines >= production_minimum}",
    )
    return RoundLineBudget(
        repositories=tuple(budgets),
        total_code_lines=total_code,
        total_technical_lines=total_technical,
        reading_minimum=reading_minimum,
        production_minimum=production_minimum,
        reading_minimum_met=total_technical >= reading_minimum,
        production_minimum_met=production_lines >= production_minimum,
        summary=summary,
    )


def budget_markdown(budget: RoundLineBudget) -> str:
    lines = ["# Budget de linhas da rodada", ""]
    lines.extend(f"- {item}" for item in budget.summary)
    lines.extend(["", "## Repositorios", ""])
    for repo in sorted(budget.repositories, key=lambda item: item.repo_name):
        lines.append(f"### {repo.repo_name}")
        lines.append("")
        lines.append(f"- existe: `{repo.exists}`")
        lines.append(f"- arquivos vistos: `{repo.files_seen}`")
        lines.append(f"- arquivos contados: `{repo.files_counted}`")
        lines.append(f"- linhas codigo: `{repo.code_lines}`")
        lines.append(f"- linhas tecnicas: `{repo.technical_lines}`")
        if repo.warnings:
            lines.append("- warnings:")
            for warning in repo.warnings:
                lines.append(f"  - {warning}")
        lines.append("- maiores arquivos:")
        for item in repo.largest_files[:8]:
            lines.append(f"  - `{item.path}`: `{item.lines}` linhas")
        lines.append("")
    return "\n".join(lines).strip() + "\n"


def budget_rows(budget: RoundLineBudget) -> list[list[str]]:
    rows = [["repo", "exists", "files_seen", "files_counted", "code_lines", "technical_lines", "warnings"]]
    for repo in sorted(budget.repositories, key=lambda item: item.repo_name):
        rows.append(
            [
                repo.repo_name,
                "yes" if repo.exists else "no",
                str(repo.files_seen),
                str(repo.files_counted),
                str(repo.code_lines),
                str(repo.technical_lines),
                " | ".join(repo.warnings),
            ]
        )
    return rows


def budget_pending_items(budget: RoundLineBudget) -> tuple[str, ...]:
    items: list[str] = []
    if not budget.reading_minimum_met:
        items.append(
            f"minimo de leitura nao cumprido: {budget.total_technical_lines}/{budget.reading_minimum}; ampliar base material ou registrar ausencia real"
        )
    if not budget.production_minimum_met:
        project = budget.repositories[0] if budget.repositories else None
        current = project.code_lines if project is not None else 0
        items.append(
            f"minimo de producao nao cumprido no projeto real: {current}/{budget.production_minimum}; implementar codigo util adicional ou justificar impossibilidade"
        )
    for repo in budget.repositories:
        for warning in repo.warnings:
            items.append(f"{repo.repo_name}: {warning}")
    return merge_unique(items)


def budget_recommendations(budget: RoundLineBudget) -> tuple[str, ...]:
    recommendations: list[str] = []
    if budget.reading_minimum_met:
        recommendations.append("manter leitura minima comprovada em reports e SQL semantico")
    else:
        recommendations.append(
            f"ampliar leitura tecnica/documental em {budget.missing_reading_lines} linhas ou registrar ausencia material"
        )
    if budget.production_minimum_met:
        recommendations.append("manter producao util de codigo acima do minimo da rodada")
    else:
        recommendations.append(
            f"produzir mais {budget.missing_production_lines} linhas uteis em Python/TS/JS/Java ou justificar impossibilidade real"
        )
    missing_repos = [repo.repo_name for repo in budget.repositories if not repo.exists]
    if missing_repos:
        recommendations.append("resolver espelhos ausentes: " + ", ".join(missing_repos[:8]))
    zero_code = [repo.repo_name for repo in budget.repositories if repo.exists and repo.code_lines == 0]
    if zero_code:
        recommendations.append("classificar repositorios sem codigo contado: " + ", ".join(zero_code[:8]))
    heavy = sorted([repo for repo in budget.repositories if repo.technical_lines > 50_000], key=lambda item: item.technical_lines, reverse=True)
    if heavy:
        recommendations.append("usar scanner com budget em repositorios grandes: " + ", ".join(repo.repo_name for repo in heavy[:5]))
    return merge_unique(recommendations)


def production_status_label(budget: RoundLineBudget) -> str:
    if budget.reading_minimum_met and budget.production_minimum_met:
        return "minimos-cumpridos"
    if budget.reading_minimum_met and not budget.production_minimum_met:
        return "leitura-ok-producao-parcial"
    if not budget.reading_minimum_met and budget.production_minimum_met:
        return "producao-ok-leitura-parcial"
    return "minimos-parciais"


def budget_status_payload(budget: RoundLineBudget) -> dict[str, object]:
    project = budget.project_budget
    return {
        "status": production_status_label(budget),
        "reading_minimum": budget.reading_minimum,
        "production_minimum": budget.production_minimum,
        "total_technical_lines": budget.total_technical_lines,
        "total_code_lines": budget.total_code_lines,
        "project_code_lines": project.code_lines if project is not None else 0,
        "missing_reading_lines": budget.missing_reading_lines,
        "missing_production_lines": budget.missing_production_lines,
        "recommendations": budget_recommendations(budget),
    }


def repository_budget_markdown(repo: RepositoryLineBudget) -> str:
    lines = [
        f"## {repo.repo_name}",
        "",
        f"- caminho: `{repo.repo_path}`",
        f"- existe: `{repo.exists}`",
        f"- arquivos vistos: `{repo.files_seen}`",
        f"- arquivos contados: `{repo.files_counted}`",
        f"- linhas codigo: `{repo.code_lines}`",
        f"- linhas tecnicas: `{repo.technical_lines}`",
    ]
    if repo.warnings:
        lines.append("- warnings:")
        lines.extend(f"  - {warning}" for warning in repo.warnings)
    if repo.largest_files:
        lines.append("- maiores arquivos tecnicos:")
        for item in repo.largest_files[:12]:
            kind = "codigo" if item.counted_as_code else "tecnico"
            lines.append(f"  - `{item.path}`: `{item.lines}` linhas ({kind})")
    return "\n".join(lines).strip() + "\n"


def budget_recommendations_markdown(budget: RoundLineBudget) -> str:
    lines = [
        "# Recomendacoes do budget de linhas",
        "",
        f"- status: `{production_status_label(budget)}`",
        f"- faltam linhas leitura: `{budget.missing_reading_lines}`",
        f"- faltam linhas producao: `{budget.missing_production_lines}`",
        "",
    ]
    for recommendation in budget_recommendations(budget):
        lines.append(f"- {recommendation}")
    return "\n".join(lines).strip() + "\n"


def top_repositories_by_code(budget: RoundLineBudget, limit: int = 10) -> tuple[RepositoryLineBudget, ...]:
    return tuple(sorted(budget.repositories, key=lambda item: item.code_lines, reverse=True)[:limit])


def top_repositories_by_technical_lines(budget: RoundLineBudget, limit: int = 10) -> tuple[RepositoryLineBudget, ...]:
    return tuple(sorted(budget.repositories, key=lambda item: item.technical_lines, reverse=True)[:limit])


def budget_focus_rows(budget: RoundLineBudget) -> list[list[str]]:
    rows = [["rank", "repo", "code_lines", "technical_lines", "warnings", "focus"]]
    for index, repo in enumerate(top_repositories_by_technical_lines(budget), start=1):
        if not repo.exists:
            focus = "recuperar espelho"
        elif repo.code_lines == 0:
            focus = "classificar ausencia de codigo"
        elif repo.technical_lines > 100_000:
            focus = "usar scanner com budget"
        else:
            focus = "manter leitura normal"
        rows.append(
            [
                str(index),
                repo.repo_name,
                str(repo.code_lines),
                str(repo.technical_lines),
                " | ".join(repo.warnings),
                focus,
            ]
        )
    return rows


def build_order_reason_from_budget(budget: RoundLineBudget) -> str:
    if budget.production_minimum_met and budget.reading_minimum_met:
        return "Minimos de leitura e producao foram cumpridos; continuidade deve focar maturidade, nao volume."
    parts: list[str] = []
    if not budget.reading_minimum_met:
        parts.append(f"leitura tecnica ficou {budget.missing_reading_lines} linhas abaixo do minimo")
    if not budget.production_minimum_met:
        parts.append(f"producao util ficou {budget.missing_production_lines} linhas abaixo do minimo")
    return "; ".join(parts) + "."


def classify_repository_size(repo: RepositoryLineBudget) -> str:
    if not repo.exists:
        return "missing"
    if repo.technical_lines >= 250_000:
        return "very_large"
    if repo.technical_lines >= 75_000:
        return "large"
    if repo.technical_lines >= 10_000:
        return "medium"
    if repo.technical_lines > 0:
        return "small"
    return "empty"


def scan_strategy_for_repo(repo: RepositoryLineBudget) -> str:
    size = classify_repository_size(repo)
    if size == "missing":
        return "recuperar ou clonar repositorio antes de exigir leitura"
    if size == "very_large":
        return "usar leitura amostrada por dominios e arquivos de contrato antes de varredura total"
    if size == "large":
        return "priorizar src, tests, contratos, README e reports recentes"
    if size == "medium":
        return "varredura completa e viavel com budget padrao"
    if size == "small":
        return "ler tudo e registrar limitacao se faltar base material"
    return "registrar ausencia de base tecnica e criar OS de estruturacao"


def scan_strategy_rows(budget: RoundLineBudget) -> list[list[str]]:
    rows = [["repo", "size", "strategy", "technical_lines", "code_lines"]]
    for repo in sorted(budget.repositories, key=lambda item: item.repo_name):
        rows.append(
            [
                repo.repo_name,
                classify_repository_size(repo),
                scan_strategy_for_repo(repo),
                str(repo.technical_lines),
                str(repo.code_lines),
            ]
        )
    return rows


def budget_strategy_markdown(budget: RoundLineBudget) -> str:
    lines = ["# Estrategia de leitura por budget", ""]
    lines.append(f"- status: `{production_status_label(budget)}`")
    lines.append(f"- repositorios: `{len(budget.repositories)}`")
    lines.append("")
    for repo in sorted(budget.repositories, key=lambda item: (classify_repository_size(item), item.repo_name)):
        lines.append(f"## {repo.repo_name}")
        lines.append("")
        lines.append(f"- tamanho: `{classify_repository_size(repo)}`")
        lines.append(f"- linhas tecnicas: `{repo.technical_lines}`")
        lines.append(f"- linhas codigo: `{repo.code_lines}`")
        lines.append(f"- estrategia: {scan_strategy_for_repo(repo)}")
        lines.append("")
    return "\n".join(lines).strip() + "\n"


def budget_repository_lookup(budget: RoundLineBudget, repo_name: str) -> RepositoryLineBudget | None:
    lowered = repo_name.lower()
    for repo in budget.repositories:
        if repo.repo_name.lower() == lowered or Path(repo.repo_path).name.lower() == lowered:
            return repo
    return None


def budget_completion_ratio(budget: RoundLineBudget) -> dict[str, float]:
    project = budget.project_budget
    project_code = project.code_lines if project is not None else 0
    reading_ratio = min(1.0, budget.total_technical_lines / budget.reading_minimum) if budget.reading_minimum else 1.0
    production_ratio = min(1.0, project_code / budget.production_minimum) if budget.production_minimum else 1.0
    return {
        "reading": round(reading_ratio, 4),
        "production": round(production_ratio, 4),
        "combined": round((reading_ratio + production_ratio) / 2, 4),
    }


def budget_completion_sentence(budget: RoundLineBudget) -> str:
    ratios = budget_completion_ratio(budget)
    return (
        f"Leitura {ratios['reading']:.0%}, producao {ratios['production']:.0%}, "
        f"completude combinada {ratios['combined']:.0%}."
    )


def budget_is_operationally_sufficient(budget: RoundLineBudget) -> bool:
    ratios = budget_completion_ratio(budget)
    return ratios["reading"] >= 1.0 and ratios["production"] >= 1.0


def budget_gate_label(budget: RoundLineBudget) -> str:
    if budget_is_operationally_sufficient(budget):
        return "pass"
    ratios = budget_completion_ratio(budget)
    if ratios["combined"] >= 0.75:
        return "attention"
    if ratios["combined"] >= 0.4:
        return "partial"
    return "blocked"


def budget_gate_summary(budget: RoundLineBudget) -> tuple[str, str]:
    return (budget_gate_label(budget), budget_completion_sentence(budget))


def budget_gate_passed(budget: RoundLineBudget) -> bool:
    return budget_gate_label(budget) == "pass"


def budget_gate_failed(budget: RoundLineBudget) -> bool:
    return not budget_gate_passed(budget)
