"""Can-Do-Steps storage-native step fixer.

Applies step-level fixes to bits JSON stored in local or remote storage.
No git operations are performed.
"""
from __future__ import annotations

import json
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from chains.base import build_chain, default_json_parser, parse_output
from chains.can_do_steps.fix_content import (
    _extract_plan_items,
    _normalize_targets,
    _build_bits_context,
    _run_apply_fix_chain,
    _extract_updated_snippet,
    _replace_bits_snippet,
)
from utils.io import save_output
from utils.storage import path_exists, read_text, read_json, copy_path, write_json


def _default_bits_path(run_id: str) -> str:
    return f"output/can-do-steps/{run_id}/bits-{run_id}.json"


def _archive_bits_path(run_id: str) -> str:
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    return f"output/can-do-steps/{run_id}/archived/bits-{run_id}-{timestamp}.json"


def _extract_plan_items_from_input(raw_instructions: str) -> Optional[List[Dict[str, Any]]]:
    """
    If the input itself already contains fix_items (or a direct list), reuse it.
    """
    try:
        payload = json.loads(raw_instructions)
    except json.JSONDecodeError:
        return None

    plan_items = _extract_plan_items(payload)
    if plan_items:
        return plan_items
    return None


def fix_step(
    run_id: str,
    fix_input: str,
    bits_file: Optional[str] = None,
    force_text: bool = False,
) -> Dict[str, Any]:
    """
    Apply step-level fixes to bits JSON and write back to storage.

    Args:
        run_id: Roadmap run identifier.
        fix_input: Review/fix instructions file (local path, key, or s3 URI).
        bits_file: Optional bits JSON path/key. Defaults to bits-{run_id}.json.
        force_text: If True, allow force-text behavior for chain calls.
    """
    if not path_exists(fix_input):
        raise FileNotFoundError(f"Fix instructions file not found: {fix_input}")

    bits_path = bits_file or _default_bits_path(run_id)
    if not path_exists(bits_path):
        raise FileNotFoundError(f"Bits file not found: {bits_path}")

    raw_instructions = read_text(fix_input)
    bits_data = read_json(bits_path)

    plan_items = _extract_plan_items_from_input(raw_instructions)
    if plan_items is None:
        plan_result = build_chain(
            chain_name="plan_fix",
            pipeline="can-do-steps",
            run_id=run_id,
            input_variables={"raw_instructions": raw_instructions},
            force_text=force_text,
        )
        plan_data = parse_output(plan_result["output"], default_json_parser)
        plan_items = _extract_plan_items(plan_data)

    save_output(
        data={"fix_items": plan_items},
        pipeline="can-do-steps",
        step="plan_fix_structured",
        run_id=run_id,
        subfolder="logs",
    )

    summary: Dict[str, Any] = {
        "run_id": run_id,
        "bits_file": bits_path,
        "planned_items": len(plan_items),
        "applied": 0,
        "skipped": [],
        "errors": [],
        "archived_copy": None,
        "bits_updated": False,
    }

    if not plan_items:
        save_output(summary, "can-do-steps", "fix_step", run_id, subfolder="logs")
        print("No actionable fixes found in instructions.")
        return summary

    # Archive original bits file once before applying updates.
    archive_path = _archive_bits_path(run_id)
    summary["archived_copy"] = copy_path(bits_path, archive_path)
    print(f"📦 Archived bits snapshot to: {summary['archived_copy']}")

    changed_any = False
    for item in plan_items:
        step_id = item.get("step_id")
        instruction = (item.get("instruction") or "").strip()
        targets = _normalize_targets(item.get("targets"))

        if "bits" not in targets:
            summary["skipped"].append(
                {"step_id": step_id or "unknown", "reason": "targets do not include bits"}
            )
            continue

        if not step_id or not instruction:
            summary["skipped"].append(
                {"step_id": step_id or "unknown", "reason": "missing step_id or instruction"}
            )
            continue

        bits_context = _build_bits_context(bits_data, step_id)
        if bits_context is None:
            summary["errors"].append(
                {"step_id": step_id, "error": "step not found in bits data"}
            )
            print(f"⚠️ Step '{step_id}' missing in bits data; skipping")
            continue

        try:
            apply_payload = _run_apply_fix_chain(
                run_id=run_id,
                step_id=step_id,
                instruction=instruction,
                targets=["bits"],
                roadmap_context=None,
                bits_context=bits_context,
                force_text=force_text,
            )
            bits_snippet_updated = _extract_updated_snippet(apply_payload, "bits_snippet_updated")
            if bits_snippet_updated is None:
                summary["skipped"].append(
                    {"step_id": step_id, "reason": "model returned no bits update"}
                )
                continue

            updated = _replace_bits_snippet(bits_data, step_id, bits_snippet_updated)
            if updated:
                changed_any = True
                summary["applied"] += 1
            else:
                summary["skipped"].append(
                    {"step_id": step_id, "reason": "no effective content change"}
                )
        except Exception as exc:  # noqa: BLE001
            summary["errors"].append({"step_id": step_id, "error": str(exc)})
            print(f"⚠️ Apply fix failed for {step_id}: {exc}")

    if changed_any:
        write_json(bits_path, bits_data)
        summary["bits_updated"] = True
        print(f"✅ Updated bits file: {bits_path}")
    else:
        print("ℹ️ No bits changes were applied.")

    save_output(summary, "can-do-steps", "fix_step", run_id, subfolder="logs")
    return summary

