from __future__ import annotations

import argparse
import json
import subprocess
import sys
import time
from datetime import UTC, datetime
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
    sys.path.insert(0, str(SCRIPTS))

import capture_1c_template_probe as probe  # noqa: E402


def read_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}


def write_json(path: Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")


def snapshot_identity(snapshot: dict[str, Any]) -> dict[str, Any]:
    return {
        "file_name": snapshot.get("file_name"),
        "modified": snapshot.get("modified"),
        "bytes": snapshot.get("bytes"),
    }


def capture_snapshot(
    *,
    adapter_url: str,
    base_id: str,
    output_dir: Path,
    label: str,
    scan_limit: int,
    max_cells: int,
    file_name: str | None = None,
    compare_to: Path | None = None,
) -> tuple[Path, Path, dict[str, Any]]:
    output_dir.mkdir(parents=True, exist_ok=True)
    selected_file, row, structure = probe.choose_latest_moxel(adapter_url, base_id, scan_limit, max_cells, file_name)
    snapshot = {
        "schema": "codex_1c_template_probe_snapshot.v1",
        "captured_at": datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z"),
        "adapter_url": adapter_url,
        "base_id": base_id,
        "file_name": selected_file,
        "modified": row.get("Modified"),
        "bytes": row.get("Bytes"),
        "label": label,
        "probe": probe.build_snapshot(structure),
    }
    diff: dict[str, Any] | None = None
    if compare_to and compare_to.exists():
        previous = read_json(compare_to)
        diff = probe.build_diff(previous.get("probe") or {}, snapshot.get("probe") or {})
        snapshot["diff"] = {"compare_to": str(compare_to), "summary": diff.get("summary") or {}}

    timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
    stem = f"{base_id}_{label}_{timestamp}_{selected_file[:8]}"
    json_path = output_dir / f"{stem}.json"
    md_path = output_dir / f"{stem}.md"
    write_json(json_path, snapshot)
    md_path.write_text(probe.render_markdown(snapshot, diff, compare_to if compare_to and compare_to.exists() else None), encoding="utf-8")
    return json_path, md_path, snapshot


def append_manifest_experiment(
    manifest_path: Path,
    *,
    property_name: str,
    operation: str,
    target_text: str | None,
    target_name: str | None,
    before_path: Path,
    after_path: Path,
) -> dict[str, Any]:
    manifest = read_json(manifest_path)
    if not manifest:
        manifest = {"schema": "codex_1c_moxel_property_experiment_manifest.v1", "experiments": []}
    manifest.setdefault("schema", "codex_1c_moxel_property_experiment_manifest.v1")
    manifest.setdefault("experiments", [])
    base_dir = manifest_path.parent
    experiment = {
        "property": property_name,
        "operation": operation,
        "target_text": target_text,
        "target_name": target_name,
        "before": str(before_path.resolve().relative_to(base_dir.resolve())) if before_path.resolve().is_relative_to(base_dir.resolve()) else str(before_path),
        "after": str(after_path.resolve().relative_to(base_dir.resolve())) if after_path.resolve().is_relative_to(base_dir.resolve()) else str(after_path),
    }
    manifest["experiments"].append(experiment)
    write_json(manifest_path, manifest)
    return experiment


def pipeline_command(after_path: Path, manifest_path: Path) -> list[str]:
    return [
        sys.executable,
        "scripts/run_1c_moxel_discovery_pipeline.py",
        "--probe",
        str(after_path),
        "--property-manifest",
        str(manifest_path),
    ]


def wait_for_new_moxel(
    *,
    adapter_url: str,
    base_id: str,
    baseline: dict[str, Any],
    scan_limit: int,
    max_cells: int,
    poll_seconds: float,
    timeout_seconds: float,
) -> tuple[str, dict[str, Any], dict[str, Any]] | None:
    started = time.monotonic()
    baseline_identity = snapshot_identity(baseline)
    while time.monotonic() - started <= timeout_seconds:
        file_name, row, structure = probe.choose_latest_moxel(adapter_url, base_id, scan_limit, max_cells, None)
        current_identity = {
            "file_name": file_name,
            "modified": row.get("Modified"),
            "bytes": row.get("Bytes"),
        }
        if current_identity != baseline_identity:
            return file_name, row, structure
        time.sleep(max(0.2, poll_seconds))
    return None


def main() -> int:
    parser = argparse.ArgumentParser(description="Capture a before/after MOXCEL one-property experiment around a manual 1C save.")
    parser.add_argument("--adapter-url", default="http://docker.cin.su:8011")
    parser.add_argument("--base-id", default="upo_test")
    parser.add_argument("--property", required=True, help="Property label, for example ВертикальноеПоложение.")
    parser.add_argument("--operation", default="manual_one_property_save")
    parser.add_argument("--target-text", default="Ячейка 7 - 2")
    parser.add_argument("--target-name", default="R7C2_TEST")
    parser.add_argument("--output-dir", default="reports/1c-template-probes")
    parser.add_argument("--manifest", default="reports/1c-template-baselines/Primer3_moxel_property_experiments.manifest.json")
    parser.add_argument("--scan-limit", type=int, default=30)
    parser.add_argument("--max-cells", type=int, default=800)
    parser.add_argument("--poll-seconds", type=float, default=5.0)
    parser.add_argument("--timeout-seconds", type=float, default=600.0)
    parser.add_argument("--baseline", help="Existing before snapshot JSON. If omitted, capture one before waiting.")
    parser.add_argument("--after-file-name", help="Explicit after ConfigCAS file name. Skips waiting.")
    parser.add_argument("--run-pipeline-after", action="store_true", help="Run the MOXCEL discovery pipeline after appending the experiment.")
    args = parser.parse_args()

    output_dir = Path(args.output_dir)
    manifest_path = Path(args.manifest)
    if args.baseline:
        before_path = Path(args.baseline)
        before_snapshot = read_json(before_path)
    else:
        before_path, _, before_snapshot = capture_snapshot(
            adapter_url=args.adapter_url,
            base_id=args.base_id,
            output_dir=output_dir,
            label=f"{args.property}_before",
            scan_limit=args.scan_limit,
            max_cells=args.max_cells,
        )

    if args.after_file_name:
        after_path, after_md, after_snapshot = capture_snapshot(
            adapter_url=args.adapter_url,
            base_id=args.base_id,
            output_dir=output_dir,
            label=f"{args.property}_after",
            scan_limit=args.scan_limit,
            max_cells=args.max_cells,
            file_name=args.after_file_name,
            compare_to=before_path,
        )
    else:
        found = wait_for_new_moxel(
            adapter_url=args.adapter_url,
            base_id=args.base_id,
            baseline=before_snapshot,
            scan_limit=args.scan_limit,
            max_cells=args.max_cells,
            poll_seconds=args.poll_seconds,
            timeout_seconds=args.timeout_seconds,
        )
        if not found:
            print(
                json.dumps(
                    {
                        "status": "no_change",
                        "before": str(before_path),
                        "message": f"No new MOXCEL payload appeared within {args.timeout_seconds:.0f} seconds.",
                    },
                    ensure_ascii=False,
                    indent=2,
                )
            )
            return 2
        file_name, _, _ = found
        after_path, after_md, after_snapshot = capture_snapshot(
            adapter_url=args.adapter_url,
            base_id=args.base_id,
            output_dir=output_dir,
            label=f"{args.property}_after",
            scan_limit=args.scan_limit,
            max_cells=args.max_cells,
            file_name=file_name,
            compare_to=before_path,
        )

    experiment = append_manifest_experiment(
        manifest_path,
        property_name=args.property,
        operation=args.operation,
        target_text=args.target_text,
        target_name=args.target_name,
        before_path=before_path,
        after_path=after_path,
    )
    pipeline: dict[str, Any] | None = None
    if args.run_pipeline_after:
        command = pipeline_command(after_path, manifest_path)
        result = subprocess.run(command, cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
        pipeline = {
            "command": command,
            "status": "ok" if result.returncode == 0 else "failed",
            "returncode": result.returncode,
            "stdout": result.stdout.strip(),
            "stderr": result.stderr.strip(),
        }
    print(
        json.dumps(
            {
                "status": "ok",
                "before": str(before_path),
                "after": str(after_path),
                "after_markdown": str(after_md),
                "manifest": str(manifest_path),
                "experiment": experiment,
                "before_identity": snapshot_identity(before_snapshot),
                "after_identity": snapshot_identity(after_snapshot),
                **({"pipeline": pipeline} if pipeline else {}),
            },
            ensure_ascii=False,
            indent=2,
        )
    )
    if pipeline and pipeline.get("returncode") != 0:
        return int(pipeline.get("returncode") or 1)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
