"""Local workspace hygiene checks for service-order closeout.

The Mais Humana rounds generate Python test scratch folders and may touch
JavaScript/Cloudflare workspaces while validating MCP publication.  This module
turns cleanup into an auditable operation instead of an informal shell step:
it verifies target paths, deletes only approved local artifacts when requested,
and writes redacted project/central reports that explain any ACL retention.
"""

from __future__ import annotations

import csv
import io
import json
import os
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any, Sequence

from .models import GeneratedFile, as_plain_data, merge_unique, utc_now


DEFAULT_LOCAL_ARTIFACTS = (
    ".test-tmp",
    "node_modules",
)


class HygieneStatus(str, Enum):
    """Result for a cleanup target or whole report."""

    PASSED = "passed"
    PARTIAL = "partial"
    BLOCKED = "blocked"
    NOT_FOUND = "not_found"
    NOT_RUN = "not_run"


class HygieneActionKind(str, Enum):
    """Action planned or executed for one local artifact."""

    DELETE_DIRECTORY = "delete_directory"
    DELETE_FILE = "delete_file"
    VERIFY_ABSENT = "verify_absent"
    BLOCK_UNSAFE_PATH = "block_unsafe_path"


@dataclass(frozen=True, slots=True)
class HygieneTarget:
    """Approved local cleanup target."""

    target_id: str
    relative_path: str
    reason: str
    required_absent: bool = True
    delete_when_apply: bool = True

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


@dataclass(frozen=True, slots=True)
class PathFootprint:
    """Best-effort path size and child count."""

    exists: bool
    is_dir: bool
    is_file: bool
    child_count: int
    byte_count: int
    errors: tuple[str, ...]

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


@dataclass(frozen=True, slots=True)
class HygieneAction:
    """Observed cleanup action for one target."""

    target_id: str
    path: str
    action: HygieneActionKind
    status: HygieneStatus
    applied: bool
    deleted: bool
    footprint_before: PathFootprint
    footprint_after: PathFootprint
    error: str = ""
    note: str = ""

    @property
    def clean(self) -> bool:
        return self.status in {HygieneStatus.PASSED, HygieneStatus.NOT_FOUND} and not self.footprint_after.exists

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


@dataclass(frozen=True, slots=True)
class WorkspaceHygieneReport:
    """Full hygiene report for a platform workspace."""

    report_id: str
    generated_at: str
    project_root: str
    central_platform_folder: str
    apply: bool
    targets: tuple[HygieneTarget, ...]
    actions: tuple[HygieneAction, ...]
    summary: tuple[str, ...]
    blockers: tuple[str, ...]

    @property
    def status(self) -> HygieneStatus:
        if any(action.status == HygieneStatus.BLOCKED for action in self.actions):
            return HygieneStatus.BLOCKED
        if any(action.status in {HygieneStatus.PARTIAL, HygieneStatus.NOT_RUN} for action in self.actions):
            return HygieneStatus.PARTIAL
        return HygieneStatus.PASSED

    @property
    def clean(self) -> bool:
        return all(action.clean for action in self.actions)

    def to_dict(self) -> dict[str, Any]:
        data = as_plain_data(self)
        data["status"] = self.status.value
        data["clean"] = self.clean
        return data


def default_hygiene_targets() -> tuple[HygieneTarget, ...]:
    """Return local artifact targets allowed for automated cleanup."""

    return (
        HygieneTarget(
            target_id="python-test-temp",
            relative_path=".test-tmp",
            reason="scratch directory created by Python unit tests; must not survive closeout",
        ),
        HygieneTarget(
            target_id="node-dependencies",
            relative_path="node_modules",
            reason="local Node dependency directory; must not be versioned or retained after local tests",
        ),
    )


def _norm(path: Path) -> str:
    return os.path.normcase(os.path.abspath(str(path)))


def safe_child_path(root: Path, relative_path: str) -> Path:
    """Resolve a target and reject paths outside the project root."""

    if Path(relative_path).is_absolute():
        raise ValueError(f"absolute cleanup target is not allowed: {relative_path}")
    root_resolved = root.resolve(strict=False)
    target = (root_resolved / relative_path).resolve(strict=False)
    try:
        common = os.path.commonpath([_norm(root_resolved), _norm(target)])
    except ValueError as exc:
        raise ValueError(f"cleanup target crosses drive boundary: {relative_path}") from exc
    if common != _norm(root_resolved):
        raise ValueError(f"cleanup target escapes project root: {relative_path}")
    return target


def _footprint(path: Path, *, max_errors: int = 8) -> PathFootprint:
    if not path.exists():
        return PathFootprint(False, False, False, 0, 0, ())
    if path.is_file():
        try:
            size = path.stat().st_size
            return PathFootprint(True, False, True, 1, int(size), ())
        except OSError as exc:
            return PathFootprint(True, False, True, 1, 0, (f"{type(exc).__name__}: {exc}",))

    child_count = 0
    byte_count = 0
    errors: list[str] = []
    for current_root, dirnames, filenames in os.walk(path, topdown=True, onerror=lambda exc: errors.append(f"{type(exc).__name__}: {exc}")):
        child_count += len(dirnames) + len(filenames)
        for filename in filenames:
            try:
                byte_count += int((Path(current_root) / filename).stat().st_size)
            except OSError as exc:
                if len(errors) < max_errors:
                    errors.append(f"{type(exc).__name__}: {exc}")
        if len(errors) >= max_errors:
            errors.append("error_limit_reached")
            break
    return PathFootprint(True, True, False, child_count, byte_count, tuple(errors[:max_errors]))


def _delete_file(path: Path, errors: list[str], *, max_errors: int = 12) -> None:
    try:
        if not path.exists():
            return
        path.unlink()
    except OSError as exc:
        if len(errors) < max_errors:
            errors.append(f"{type(exc).__name__}: {exc}")


def _delete_empty_dir(path: Path, errors: list[str], *, max_errors: int = 12) -> None:
    try:
        if path.exists():
            path.rmdir()
    except OSError as exc:
        if len(errors) < max_errors:
            errors.append(f"{type(exc).__name__}: {exc}")


def _delete_directory_best_effort(path: Path) -> tuple[bool, str]:
    """Delete a directory while continuing after ACL errors.

    ``shutil.rmtree`` stops at the first inaccessible child on Windows.  The
    closeout hygiene needs a better operational behavior: remove every
    accessible file/directory, retain only the ACL-blocked paths, and report the
    retained evidence.
    """

    errors: list[str] = []

    def on_walk_error(exc: OSError) -> None:
        if len(errors) < 12:
            errors.append(f"{type(exc).__name__}: {exc}")

    for current_root, dirnames, filenames in os.walk(path, topdown=False, onerror=on_walk_error):
        current = Path(current_root)
        for filename in filenames:
            _delete_file(current / filename, errors)
        for dirname in dirnames:
            _delete_empty_dir(current / dirname, errors)
    _delete_empty_dir(path, errors)
    if not path.exists():
        return True, ""
    message = "; ".join(merge_unique(errors))
    if not message:
        message = "path retained after best-effort cleanup"
    return False, message


def _delete_path(path: Path) -> tuple[bool, str]:
    try:
        if not path.exists():
            return False, ""
        if path.is_dir():
            return _delete_directory_best_effort(path)
        path.unlink()
        return True, ""
    except OSError as exc:
        return False, f"{type(exc).__name__}: {exc}"


def run_hygiene_actions(
    project_root: Path,
    *,
    apply: bool = False,
    targets: Sequence[HygieneTarget] | None = None,
) -> tuple[HygieneAction, ...]:
    """Inspect and optionally remove approved local artifacts."""

    action_rows: list[HygieneAction] = []
    for target in tuple(targets or default_hygiene_targets()):
        try:
            path = safe_child_path(project_root, target.relative_path)
        except ValueError as exc:
            empty = PathFootprint(False, False, False, 0, 0, ())
            action_rows.append(
                HygieneAction(
                    target_id=target.target_id,
                    path=str(project_root / target.relative_path),
                    action=HygieneActionKind.BLOCK_UNSAFE_PATH,
                    status=HygieneStatus.BLOCKED,
                    applied=False,
                    deleted=False,
                    footprint_before=empty,
                    footprint_after=empty,
                    error=str(exc),
                    note="unsafe path blocked before filesystem write",
                )
            )
            continue

        before = _footprint(path)
        if not before.exists:
            action_rows.append(
                HygieneAction(
                    target_id=target.target_id,
                    path=str(path),
                    action=HygieneActionKind.VERIFY_ABSENT,
                    status=HygieneStatus.NOT_FOUND,
                    applied=False,
                    deleted=False,
                    footprint_before=before,
                    footprint_after=before,
                    note="target already absent",
                )
            )
            continue

        kind = HygieneActionKind.DELETE_DIRECTORY if before.is_dir else HygieneActionKind.DELETE_FILE
        if not apply or not target.delete_when_apply:
            action_rows.append(
                HygieneAction(
                    target_id=target.target_id,
                    path=str(path),
                    action=kind,
                    status=HygieneStatus.NOT_RUN,
                    applied=False,
                    deleted=False,
                    footprint_before=before,
                    footprint_after=before,
                    note="dry run; use --apply to remove approved artifact",
                )
            )
            continue

        deleted, error = _delete_path(path)
        after = _footprint(path)
        status = HygieneStatus.PASSED if deleted and not after.exists else HygieneStatus.BLOCKED
        note = "removed approved local artifact" if status == HygieneStatus.PASSED else "artifact retained by ACL or filesystem lock"
        action_rows.append(
            HygieneAction(
                target_id=target.target_id,
                path=str(path),
                action=kind,
                status=status,
                applied=True,
                deleted=deleted,
                footprint_before=before,
                footprint_after=after,
                error=error,
                note=note,
            )
        )
    return tuple(action_rows)


def build_workspace_hygiene_report(
    project_root: Path,
    *,
    central_platform_folder: Path | None = None,
    apply: bool = False,
    targets: Sequence[HygieneTarget] | None = None,
) -> WorkspaceHygieneReport:
    """Build a hygiene report for project closeout."""

    target_set = tuple(targets or default_hygiene_targets())
    actions = run_hygiene_actions(project_root, apply=apply, targets=target_set)
    blockers = merge_unique(
        f"{action.target_id}:{action.error or action.note}"
        for action in actions
        if action.status == HygieneStatus.BLOCKED
    )
    removed = sum(1 for action in actions if action.deleted)
    already_absent = sum(1 for action in actions if action.status == HygieneStatus.NOT_FOUND)
    retained = sum(1 for action in actions if action.footprint_after.exists)
    summary = (
        f"Targets evaluated: {len(actions)}.",
        f"Apply mode: {apply}.",
        f"Removed artifacts: {removed}.",
        f"Already absent: {already_absent}.",
        f"Artifacts still present: {retained}.",
        "Only approved project-local artifacts are eligible for deletion.",
    )
    seed = json.dumps(
        {
            "projectRoot": str(project_root),
            "actions": [action.to_dict() for action in actions],
            "generatedAt": utc_now(),
        },
        ensure_ascii=False,
        sort_keys=True,
    )
    report_id = "workspace-hygiene-" + str(abs(hash(seed)))[:12]
    return WorkspaceHygieneReport(
        report_id=report_id,
        generated_at=utc_now(),
        project_root=str(project_root),
        central_platform_folder=str(central_platform_folder or ""),
        apply=apply,
        targets=target_set,
        actions=actions,
        summary=summary,
        blockers=blockers,
    )


def hygiene_csv(report: WorkspaceHygieneReport) -> str:
    """Render hygiene target status as CSV."""

    rows = [["target_id", "path", "action", "status", "applied", "deleted", "exists_after", "children_before", "bytes_before", "error", "note"]]
    for action in report.actions:
        rows.append(
            [
                action.target_id,
                action.path,
                action.action.value,
                action.status.value,
                "yes" if action.applied else "no",
                "yes" if action.deleted else "no",
                "yes" if action.footprint_after.exists else "no",
                str(action.footprint_before.child_count),
                str(action.footprint_before.byte_count),
                action.error,
                action.note,
            ]
        )
    buffer = io.StringIO()
    writer = csv.writer(buffer, lineterminator="\n")
    writer.writerows(rows)
    return buffer.getvalue()


def hygiene_markdown(report: WorkspaceHygieneReport) -> str:
    """Render human-readable hygiene evidence."""

    lines = [
        "# Workspace Hygiene Report",
        "",
        f"- report_id: `{report.report_id}`",
        f"- generated_at: `{report.generated_at}`",
        f"- project_root: `{report.project_root}`",
        f"- central_platform_folder: `{report.central_platform_folder}`",
        f"- status: `{report.status.value}`",
        f"- clean: `{report.clean}`",
        f"- apply: `{report.apply}`",
        "",
        "## Summary",
        "",
    ]
    lines.extend(f"- {item}" for item in report.summary)
    lines.extend(["", "## Targets", ""])
    for action in report.actions:
        lines.extend(
            [
                f"### {action.target_id}",
                "",
                f"- path: `{action.path}`",
                f"- action: `{action.action.value}`",
                f"- status: `{action.status.value}`",
                f"- applied: `{action.applied}`",
                f"- deleted: `{action.deleted}`",
                f"- exists_after: `{action.footprint_after.exists}`",
                f"- children_before: `{action.footprint_before.child_count}`",
                f"- bytes_before: `{action.footprint_before.byte_count}`",
                f"- note: {action.note}",
            ]
        )
        if action.error:
            lines.append(f"- error: `{action.error}`")
        if action.footprint_before.errors:
            lines.append("- footprint_errors:")
            lines.extend(f"  - `{item}`" for item in action.footprint_before.errors)
        lines.append("")
    lines.extend(["## Blockers", ""])
    if report.blockers:
        lines.extend(f"- `{item}`" for item in report.blockers)
    else:
        lines.append("- Nenhum blocker de higiene local.")
    return "\n".join(lines).strip() + "\n"


def hygiene_artifact_records(project_root: Path, central_platform_folder: Path | None = None) -> tuple[GeneratedFile, ...]:
    """Return semantic records for hygiene artifacts."""

    records = [
        GeneratedFile(
            path=str(project_root / "dados" / "workspace-hygiene-report.json"),
            description="Relatorio estruturado de limpeza operacional local.",
            function="workspace hygiene report",
            file_type="json",
            changed_by="mais_humana.workspace_hygiene",
            change_summary="Registrada limpeza de .test-tmp e node_modules com bloqueios ACL quando houver.",
            relation_to_order="0036_EXECUTIVA__normalizar-limpeza-test-tmp-e-acl-local",
        ),
        GeneratedFile(
            path=str(project_root / "matrizes" / "workspace-hygiene-targets.csv"),
            description="Matriz de alvos de higiene local e estado final.",
            function="workspace hygiene matrix",
            file_type="csv",
            changed_by="mais_humana.workspace_hygiene",
            change_summary="Criada matriz auditavel de artefatos locais removidos, ausentes ou retidos.",
            relation_to_order="0036_EXECUTIVA__normalizar-limpeza-test-tmp-e-acl-local",
        ),
        GeneratedFile(
            path=str(project_root / "ecossistema" / "WORKSPACE-HYGIENE-REPORT.md"),
            description="Relatorio humano de higiene local da rodada.",
            function="workspace hygiene human report",
            file_type="markdown",
            changed_by="mais_humana.workspace_hygiene",
            change_summary="Criado relatorio de fechamento de limpeza operacional.",
            relation_to_order="0036_EXECUTIVA__normalizar-limpeza-test-tmp-e-acl-local",
        ),
    ]
    if central_platform_folder is not None:
        records.append(
            GeneratedFile(
                path=str(central_platform_folder / "reports" / "EXECUTADO__workspace-hygiene.md"),
                description="Copia central da higiene local da plataforma Mais Humana.",
                function="workspace hygiene central report",
                file_type="markdown",
                changed_by="mais_humana.workspace_hygiene",
                change_summary="Registrado estado de .test-tmp e node_modules na pasta central.",
                relation_to_order="0036_EXECUTIVA__normalizar-limpeza-test-tmp-e-acl-local",
            )
        )
    return tuple(records)


def write_hygiene_artifacts(
    report: WorkspaceHygieneReport,
    project_root: Path,
    *,
    central_platform_folder: Path | None = None,
) -> tuple[GeneratedFile, ...]:
    """Write project and optional central hygiene artifacts."""

    targets: list[tuple[Path, str]] = [
        (project_root / "dados" / "workspace-hygiene-report.json", json.dumps(report.to_dict(), ensure_ascii=False, indent=2, sort_keys=True)),
        (project_root / "matrizes" / "workspace-hygiene-targets.csv", hygiene_csv(report)),
        (project_root / "ecossistema" / "WORKSPACE-HYGIENE-REPORT.md", hygiene_markdown(report)),
    ]
    records = list(hygiene_artifact_records(project_root, central_platform_folder))
    central_failures: list[dict[str, str]] = []
    if central_platform_folder is not None:
        targets.append((central_platform_folder / "reports" / "EXECUTADO__workspace-hygiene.md", hygiene_markdown(report)))
    for path, content in targets:
        try:
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text(content, encoding="utf-8")
        except OSError as exc:
            if central_platform_folder is not None and central_platform_folder in path.parents:
                central_failures.append({"path": str(path), "error": f"{type(exc).__name__}: {exc}"})
                continue
            raise
    if central_failures:
        status_path = project_root / "dados" / "workspace-hygiene-central-write-status.json"
        status_path.write_text(
            json.dumps(
                {
                    "generatedAt": utc_now(),
                    "centralPlatformFolder": str(central_platform_folder),
                    "ok": False,
                    "failures": central_failures,
                },
                ensure_ascii=False,
                indent=2,
                sort_keys=True,
            ),
            encoding="utf-8",
        )
        records.append(
            GeneratedFile(
                path=str(status_path),
                description="Status de escrita central do relatorio de higiene local.",
                function="workspace hygiene central write status",
                file_type="json",
                changed_by="mais_humana.workspace_hygiene",
                change_summary="Registrada falha de escrita central sem abortar artefatos do projeto real.",
                relation_to_order="0034_EXECUTIVA__corrigir-acl-escrita-central-e-sql-semantico-plataforma-15",
            )
        )
    return tuple(records)


def run_workspace_hygiene(
    *,
    project_root: Path,
    central_platform_folder: Path | None = None,
    apply: bool = False,
) -> tuple[WorkspaceHygieneReport, tuple[GeneratedFile, ...]]:
    """Execute the hygiene gate and write artifacts."""

    report = build_workspace_hygiene_report(project_root, central_platform_folder=central_platform_folder, apply=apply)
    records = write_hygiene_artifacts(report, project_root, central_platform_folder=central_platform_folder)
    return report, records
