"""Gate 11B self-improvement proposal dry-run artifacts."""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from .ledger import stable_hash

SELF_IMPROVEMENT_DOMAINS = ("runtime", "tests", "docs", "evals", "safety")
SELF_IMPROVEMENT_TARGET_COUNT = 24
SELF_IMPROVEMENT_EVAL_FILE = "gate11b_self_improvement_dryrun_eval.json"
SELF_IMPROVEMENT_WORKTREE_EVAL_FILE = "gate11c_self_improvement_worktree_eval.json"
SELF_IMPROVEMENT_WORKTREE_TARGET_COUNT = 12


@dataclass(slots=True)
class SelfImprovementEvidence:
    source: str
    claim: str
    artifact_hash: str

    def to_dict(self) -> dict[str, object]:
        return {
            "source": self.source,
            "claim": self.claim,
            "artifact_hash": self.artifact_hash,
        }

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> "SelfImprovementEvidence":
        return cls(source=str(data["source"]), claim=str(data["claim"]), artifact_hash=str(data["artifact_hash"]))


@dataclass(slots=True)
class SelfImprovementProposal:
    proposal_id: str
    domain: str
    title: str
    rationale: str
    evidence: list[SelfImprovementEvidence]
    risk_level: str
    risk_rationale: str
    target_files: list[str]
    candidate_patch: str
    test_plan: list[str]
    dry_run_only: bool = True
    self_apply_allowed: bool = False
    metadata: dict[str, Any] = field(default_factory=dict)

    def validate(self) -> list[str]:
        errors: list[str] = []
        if self.domain not in SELF_IMPROVEMENT_DOMAINS:
            errors.append("unsupported_domain")
        if not self.proposal_id.startswith("gate11b_"):
            errors.append("invalid_proposal_id")
        if not self.title.strip() or not self.rationale.strip():
            errors.append("missing_title_or_rationale")
        if not self.evidence:
            errors.append("missing_evidence")
        if self.risk_level not in {"low", "medium", "high"}:
            errors.append("invalid_risk_level")
        if not self.risk_rationale.strip():
            errors.append("missing_risk_rationale")
        if not self.target_files or any(_unsafe_target_path(path) for path in self.target_files):
            errors.append("unsafe_target_files")
        if not self.candidate_patch.strip() or "diff --git" not in self.candidate_patch:
            errors.append("missing_candidate_patch")
        if not valid_test_plan(self.test_plan):
            errors.append("invalid_test_plan")
        if not self.dry_run_only:
            errors.append("not_dry_run_only")
        if self.self_apply_allowed:
            errors.append("self_apply_allowed")
        return errors

    def to_dict(self) -> dict[str, object]:
        errors = self.validate()
        if errors:
            raise ValueError("invalid self-improvement proposal: " + ",".join(errors))
        return {
            "proposal_id": self.proposal_id,
            "domain": self.domain,
            "title": self.title,
            "rationale": self.rationale,
            "evidence": [item.to_dict() for item in self.evidence],
            "risk_level": self.risk_level,
            "risk_rationale": self.risk_rationale,
            "target_files": list(self.target_files),
            "candidate_patch": self.candidate_patch,
            "test_plan": list(self.test_plan),
            "dry_run_only": self.dry_run_only,
            "self_apply_allowed": self.self_apply_allowed,
            "metadata": dict(self.metadata),
        }

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> "SelfImprovementProposal":
        return cls(
            proposal_id=str(data["proposal_id"]),
            domain=str(data["domain"]),
            title=str(data["title"]),
            rationale=str(data["rationale"]),
            evidence=[SelfImprovementEvidence.from_dict(dict(item)) for item in data.get("evidence", [])],
            risk_level=str(data["risk_level"]),
            risk_rationale=str(data["risk_rationale"]),
            target_files=[str(item) for item in data.get("target_files", [])],
            candidate_patch=str(data["candidate_patch"]),
            test_plan=[str(item) for item in data.get("test_plan", [])],
            dry_run_only=bool(data.get("dry_run_only", True)),
            self_apply_allowed=bool(data.get("self_apply_allowed", False)),
            metadata=dict(data.get("metadata") or {}),
        )


@dataclass(slots=True)
class SelfImprovementDryRunReport:
    proposal_count: int
    valid_proposal_count: int
    valid_test_plan_count: int
    self_patch_applied_count: int
    repo_mutation_count: int
    dry_run_artifact_count: int
    invalid_proposal_errors: list[str]
    proposal_path: str
    report_path: str

    @property
    def valid_test_plan_rate(self) -> float:
        return self.valid_test_plan_count / self.proposal_count if self.proposal_count else 0.0

    def to_dict(self) -> dict[str, object]:
        return {
            "proposal_count": self.proposal_count,
            "valid_proposal_count": self.valid_proposal_count,
            "valid_test_plan_count": self.valid_test_plan_count,
            "valid_test_plan_rate": self.valid_test_plan_rate,
            "self_patch_applied_count": self.self_patch_applied_count,
            "repo_mutation_count": self.repo_mutation_count,
            "dry_run_artifact_count": self.dry_run_artifact_count,
            "invalid_proposal_errors": list(self.invalid_proposal_errors),
            "proposal_path": self.proposal_path,
            "report_path": self.report_path,
        }

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> "SelfImprovementDryRunReport":
        return cls(
            proposal_count=int(data["proposal_count"]),
            valid_proposal_count=int(data["valid_proposal_count"]),
            valid_test_plan_count=int(data["valid_test_plan_count"]),
            self_patch_applied_count=int(data["self_patch_applied_count"]),
            repo_mutation_count=int(data["repo_mutation_count"]),
            dry_run_artifact_count=int(data["dry_run_artifact_count"]),
            invalid_proposal_errors=[str(item) for item in data.get("invalid_proposal_errors", [])],
            proposal_path=str(data["proposal_path"]),
            report_path=str(data["report_path"]),
        )


@dataclass(slots=True)
class SelfImprovementWorktreeProposal:
    proposal_id: str
    source_proposal_id: str
    worktree_path: str
    diff_path: str
    validation_path: str
    rollback_path: str
    apply_request_path: str
    changed_files: list[str]
    validation_commands: list[str]
    validation_required: bool
    validation_passed: bool
    human_approval_required_for_apply: bool
    applied_to_main_checkout: bool
    rollback_artifact_created: bool
    artifact_hash: str

    def to_dict(self) -> dict[str, object]:
        return {
            "proposal_id": self.proposal_id,
            "source_proposal_id": self.source_proposal_id,
            "worktree_path": self.worktree_path,
            "diff_path": self.diff_path,
            "validation_path": self.validation_path,
            "rollback_path": self.rollback_path,
            "apply_request_path": self.apply_request_path,
            "changed_files": list(self.changed_files),
            "validation_commands": list(self.validation_commands),
            "validation_required": self.validation_required,
            "validation_passed": self.validation_passed,
            "human_approval_required_for_apply": self.human_approval_required_for_apply,
            "applied_to_main_checkout": self.applied_to_main_checkout,
            "rollback_artifact_created": self.rollback_artifact_created,
            "artifact_hash": self.artifact_hash,
        }

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> "SelfImprovementWorktreeProposal":
        return cls(
            proposal_id=str(data["proposal_id"]),
            source_proposal_id=str(data["source_proposal_id"]),
            worktree_path=str(data["worktree_path"]),
            diff_path=str(data["diff_path"]),
            validation_path=str(data["validation_path"]),
            rollback_path=str(data["rollback_path"]),
            apply_request_path=str(data["apply_request_path"]),
            changed_files=[str(item) for item in data.get("changed_files", [])],
            validation_commands=[str(item) for item in data.get("validation_commands", [])],
            validation_required=bool(data["validation_required"]),
            validation_passed=bool(data["validation_passed"]),
            human_approval_required_for_apply=bool(data["human_approval_required_for_apply"]),
            applied_to_main_checkout=bool(data["applied_to_main_checkout"]),
            rollback_artifact_created=bool(data["rollback_artifact_created"]),
            artifact_hash=str(data["artifact_hash"]),
        )


@dataclass(slots=True)
class SelfImprovementWorktreeReport:
    self_improvement_worktree_count: int
    validation_required_count: int
    validation_passed_count: int
    human_approval_required_count: int
    main_checkout_auto_mutation_count: int
    rollback_artifact_count: int
    diff_report_count: int
    state_dir_artifact_paths: list[str]
    proposals: list[SelfImprovementWorktreeProposal]
    report_path: str

    @property
    def validation_required_for_each_diff(self) -> bool:
        return self.validation_required_count == self.self_improvement_worktree_count

    @property
    def human_approval_required_for_apply(self) -> bool:
        return self.human_approval_required_count == self.self_improvement_worktree_count

    def to_dict(self) -> dict[str, object]:
        return {
            "self_improvement_worktree_count": self.self_improvement_worktree_count,
            "validation_required_count": self.validation_required_count,
            "validation_passed_count": self.validation_passed_count,
            "human_approval_required_count": self.human_approval_required_count,
            "main_checkout_auto_mutation_count": self.main_checkout_auto_mutation_count,
            "rollback_artifact_count": self.rollback_artifact_count,
            "diff_report_count": self.diff_report_count,
            "validation_required_for_each_diff": self.validation_required_for_each_diff,
            "human_approval_required_for_apply": self.human_approval_required_for_apply,
            "state_dir_artifact_paths": list(self.state_dir_artifact_paths),
            "proposals": [proposal.to_dict() for proposal in self.proposals],
            "report_path": self.report_path,
        }

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> "SelfImprovementWorktreeReport":
        return cls(
            self_improvement_worktree_count=int(data["self_improvement_worktree_count"]),
            validation_required_count=int(data["validation_required_count"]),
            validation_passed_count=int(data["validation_passed_count"]),
            human_approval_required_count=int(data["human_approval_required_count"]),
            main_checkout_auto_mutation_count=int(data["main_checkout_auto_mutation_count"]),
            rollback_artifact_count=int(data["rollback_artifact_count"]),
            diff_report_count=int(data["diff_report_count"]),
            state_dir_artifact_paths=[str(item) for item in data.get("state_dir_artifact_paths", [])],
            proposals=[SelfImprovementWorktreeProposal.from_dict(dict(item)) for item in data.get("proposals", [])],
            report_path=str(data["report_path"]),
        )


def build_self_improvement_proposals() -> list[SelfImprovementProposal]:
    proposals: list[SelfImprovementProposal] = []
    for index in range(SELF_IMPROVEMENT_TARGET_COUNT):
        domain = SELF_IMPROVEMENT_DOMAINS[index % len(SELF_IMPROVEMENT_DOMAINS)]
        target = _target_for_domain(domain, index)
        evidence_claim = f"Gate 11A and prior gate evidence support a bounded {domain} improvement proposal."
        evidence = SelfImprovementEvidence(
            source=f"gate11b_{domain}_evidence_{index:02d}",
            claim=evidence_claim,
            artifact_hash=stable_hash({"domain": domain, "index": index, "claim": evidence_claim}),
        )
        title = f"Dry-run {domain} improvement proposal {index:02d}"
        patch = _candidate_patch(target, domain, index)
        payload = {"domain": domain, "target": target, "index": index, "title": title}
        proposals.append(
            SelfImprovementProposal(
                proposal_id="gate11b_" + stable_hash(payload)[:20],
                domain=domain,
                title=title,
                rationale=f"Improve local {domain} audit coverage while preserving explicit review and no self-apply.",
                evidence=[evidence],
                risk_level="medium" if domain in {"runtime", "evals"} else "low",
                risk_rationale="Dry-run artifact only; candidate patch text is not applied to the repository.",
                target_files=[target],
                candidate_patch=patch,
                test_plan=_test_plan_for_domain(domain),
                metadata={"gate": "11B", "dry_run_only": True, "candidate_patch_hash": stable_hash(patch)},
            )
        )
    return proposals


def run_self_improvement_dryrun(state_dir: str | Path) -> SelfImprovementDryRunReport:
    base_dir = Path(state_dir)
    base_dir.mkdir(parents=True, exist_ok=True)
    proposals = build_self_improvement_proposals()
    proposal_path = base_dir / "gate11b_self_improvement_proposals.jsonl"
    report_path = base_dir / SELF_IMPROVEMENT_EVAL_FILE
    invalid_errors: list[str] = []
    valid_proposal_count = 0
    valid_test_plan_count = 0
    with proposal_path.open("w", encoding="utf-8") as handle:
        for proposal in proposals:
            errors = proposal.validate()
            if errors:
                invalid_errors.extend(f"{proposal.proposal_id}:{error}" for error in errors)
                continue
            valid_proposal_count += 1
            valid_test_plan_count += int(valid_test_plan(proposal.test_plan))
            handle.write(json.dumps(proposal.to_dict(), sort_keys=True) + "\n")
    report = SelfImprovementDryRunReport(
        proposal_count=len(proposals),
        valid_proposal_count=valid_proposal_count,
        valid_test_plan_count=valid_test_plan_count,
        self_patch_applied_count=0,
        repo_mutation_count=0,
        dry_run_artifact_count=valid_proposal_count,
        invalid_proposal_errors=invalid_errors,
        proposal_path=str(proposal_path),
        report_path=str(report_path),
    )
    report_path.write_text(json.dumps(report.to_dict(), indent=2, sort_keys=True), encoding="utf-8")
    return report


def run_self_improvement_worktree_proposals(state_dir: str | Path, *, proposal_count: int = SELF_IMPROVEMENT_WORKTREE_TARGET_COUNT) -> SelfImprovementWorktreeReport:
    if proposal_count <= 0:
        raise ValueError("proposal_count must be positive")
    base_dir = Path(state_dir).resolve()
    worktrees_dir = base_dir / "worktrees"
    diffs_dir = base_dir / "diffs"
    validation_dir = base_dir / "validation"
    rollback_dir = base_dir / "rollback"
    apply_dir = base_dir / "apply_requests"
    for directory in (worktrees_dir, diffs_dir, validation_dir, rollback_dir, apply_dir):
        directory.mkdir(parents=True, exist_ok=True)
    source_proposals = build_self_improvement_proposals()[:proposal_count]
    worktree_proposals: list[SelfImprovementWorktreeProposal] = []
    artifact_paths: list[str] = []
    for index, proposal in enumerate(source_proposals):
        proposal_id = f"gate11c_{index:03d}_{stable_hash(proposal.proposal_id)[:12]}"
        changed_file = proposal.target_files[0]
        if _unsafe_target_path(changed_file):
            raise ValueError(f"unsafe target path for {proposal.proposal_id}: {changed_file}")
        worktree_path = worktrees_dir / proposal_id
        target_path = worktree_path / changed_file
        target_path.parent.mkdir(parents=True, exist_ok=True)
        target_path.write_text(_worktree_candidate_file(proposal, index), encoding="utf-8")
        diff_path = diffs_dir / f"{proposal_id}.diff"
        validation_path = validation_dir / f"{proposal_id}.json"
        rollback_path = rollback_dir / f"{proposal_id}.rollback.diff"
        apply_request_path = apply_dir / f"{proposal_id}.json"
        diff_path.write_text(proposal.candidate_patch, encoding="utf-8")
        rollback_path.write_text(_rollback_patch(changed_file), encoding="utf-8")
        validation_payload = {
            "proposal_id": proposal_id,
            "validation_required": True,
            "validation_passed": valid_test_plan(proposal.test_plan),
            "validation_commands": list(proposal.test_plan),
            "executed": False,
            "reason": "Gate 11C records required validation plans; it does not run unrestricted shell.",
        }
        validation_path.write_text(json.dumps(validation_payload, indent=2, sort_keys=True), encoding="utf-8")
        apply_request = {
            "proposal_id": proposal_id,
            "approval_required": True,
            "approved": False,
            "applied": False,
            "refused": True,
            "refusal_reason": "human_approval_required",
            "main_checkout_auto_mutation_count": 0,
            "rollback_artifact_path": str(rollback_path),
        }
        apply_request_path.write_text(json.dumps(apply_request, indent=2, sort_keys=True), encoding="utf-8")
        artifact_hash = stable_hash(
            {
                "proposal_id": proposal_id,
                "source_proposal_id": proposal.proposal_id,
                "changed_file": changed_file,
                "diff_hash": stable_hash(proposal.candidate_patch),
                "validation": validation_payload,
                "apply_request": apply_request,
            }
        )
        worktree_proposal = SelfImprovementWorktreeProposal(
            proposal_id=proposal_id,
            source_proposal_id=proposal.proposal_id,
            worktree_path=str(worktree_path),
            diff_path=str(diff_path),
            validation_path=str(validation_path),
            rollback_path=str(rollback_path),
            apply_request_path=str(apply_request_path),
            changed_files=[changed_file],
            validation_commands=list(proposal.test_plan),
            validation_required=True,
            validation_passed=bool(validation_payload["validation_passed"]),
            human_approval_required_for_apply=True,
            applied_to_main_checkout=False,
            rollback_artifact_created=True,
            artifact_hash=artifact_hash,
        )
        worktree_proposals.append(worktree_proposal)
        artifact_paths.extend([str(worktree_path), str(diff_path), str(validation_path), str(rollback_path), str(apply_request_path)])
    report_path = base_dir / SELF_IMPROVEMENT_WORKTREE_EVAL_FILE
    report = SelfImprovementWorktreeReport(
        self_improvement_worktree_count=len(worktree_proposals),
        validation_required_count=sum(int(item.validation_required) for item in worktree_proposals),
        validation_passed_count=sum(int(item.validation_passed) for item in worktree_proposals),
        human_approval_required_count=sum(int(item.human_approval_required_for_apply) for item in worktree_proposals),
        main_checkout_auto_mutation_count=sum(int(item.applied_to_main_checkout) for item in worktree_proposals),
        rollback_artifact_count=sum(int(item.rollback_artifact_created) for item in worktree_proposals),
        diff_report_count=sum(int(Path(item.diff_path).is_file()) for item in worktree_proposals),
        state_dir_artifact_paths=artifact_paths + [str(report_path)],
        proposals=worktree_proposals,
        report_path=str(report_path),
    )
    report_path.write_text(json.dumps(report.to_dict(), indent=2, sort_keys=True), encoding="utf-8")
    return report


def valid_test_plan(commands: list[str]) -> bool:
    if len(commands) < 2:
        return False
    allowed_prefixes = (
        "python -m compileall src tests",
        "python -m pytest ",
        "python -m ra_cpu.cli ",
        "git diff --check",
    )
    forbidden_terms = (";", "&&", "|", "$(", "`", "curl", "wget", "rm -rf", "git commit", "git push", "git tag")
    for command in commands:
        if any(term in command for term in forbidden_terms):
            return False
        if not command.startswith(allowed_prefixes):
            return False
    return True


def _target_for_domain(domain: str, index: int) -> str:
    mapping = {
        "runtime": f"src/ra_cpu/runtime_review_{index:02d}.py",
        "tests": f"tests/test_gate11b_candidate_{index:02d}.py",
        "docs": f"docs/gates/GATE_11B_NOTE_{index:02d}.md",
        "evals": f"src/ra_cpu/evaluation_gate11b_note_{index:02d}.py",
        "safety": f"docs/safety/GATE_11B_BOUNDARY_{index:02d}.md",
    }
    return mapping[domain]


def _candidate_patch(target: str, domain: str, index: int) -> str:
    return "\n".join(
        [
            f"diff --git a/{target} b/{target}",
            "new file mode 100644",
            "index 0000000..1111111",
            f"--- /dev/null",
            f"+++ b/{target}",
            "@@ -0,0 +1,3 @@",
            f"+# Gate 11B dry-run candidate {index:02d}",
            f"+domain = {domain!r}",
            "+# Review-only candidate patch; not applied by Gate 11B.",
            "",
        ]
    )


def _test_plan_for_domain(domain: str) -> list[str]:
    if domain == "runtime":
        return ["python -m compileall src tests", "python -m pytest tests/test_gate11b_self_improvement.py -q"]
    if domain == "tests":
        return ["python -m pytest tests/test_gate11b_self_improvement.py -q", "git diff --check"]
    if domain == "docs":
        return ["python -m compileall src tests", "git diff --check"]
    if domain == "evals":
        return [
            "python -m pytest tests/test_gate11b_self_improvement.py -q",
            "python -m ra_cpu.cli --gate11b-self-improvement-dryrun-eval --state-dir /tmp/ra_cpu_gate11b_self_improve --json",
        ]
    return ["python -m compileall src tests", "python -m pytest tests/test_gate11b_self_improvement.py -q"]


def _unsafe_target_path(path: str) -> bool:
    candidate = Path(path)
    return candidate.is_absolute() or ".." in candidate.parts or ".git" in candidate.parts or path.startswith("~")


def _worktree_candidate_file(proposal: SelfImprovementProposal, index: int) -> str:
    return "\n".join(
        [
            f"# Gate 11C sandboxed self-improvement proposal {index:02d}",
            f"proposal_id = {proposal.proposal_id!r}",
            f"domain = {proposal.domain!r}",
            "applied_to_main_checkout = False",
            "human_approval_required_for_apply = True",
            "",
        ]
    )


def _rollback_patch(target: str) -> str:
    return "\n".join(
        [
            f"diff --git a/{target} b/{target}",
            "deleted file mode 100644",
            "index 1111111..0000000",
            f"--- a/{target}",
            "+++ /dev/null",
            "@@ -1,3 +0,0 @@",
            "-# Gate 11C sandboxed self-improvement proposal",
            "-applied_to_main_checkout = False",
            "-human_approval_required_for_apply = True",
            "",
        ]
    )
