#!/usr/bin/env python3
from contextlib import contextmanager
from datetime import datetime, timezone
import getpass
import os
import platform
import re
import socket
import sys
import threading
import time
from pathlib import Path
from typing import Optional

try:
    import fcntl
except ImportError:  # pragma: no cover
    fcntl = None

try:
    import msvcrt
except ImportError:  # pragma: no cover
    msvcrt = None

PLAN_STATUS_START = "<!-- plan-status:start -->"
PLAN_STATUS_END = "<!-- plan-status:end -->"
WORKFLOW_STATE_START = "<!-- workflow-state:start -->"
WORKFLOW_STATE_END = "<!-- workflow-state:end -->"
PLAN_FILE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})-.+\.md$")
PLAN_LINE_RE = re.compile(
    r"^- \[(?P<check>[ xX])\] `(?P<plan>[^`]+)` "
    r"(?P<status>done|blocked|pending|in-progress|skipped)"
    r"(?:: (?P<note>.*))?$"
)
FINISH_STATUSES = {"done", "blocked", "skipped"}
ENV_BLOCKED_RE = re.compile(r"^env:([^:]+):(.+)$")
PLAN_META_REQUIRED_FIELDS = (
    "Verification Scope",
    "Verification Gate",
)
THREAD_LOCKS: dict[str, threading.Lock] = {}
THREAD_LOCKS_GUARD = threading.Lock()


def usage() -> str:
    return (
        "Usage:\n"
        "  python scripts/main_loop.py claim -plans <dir> -progress <file>\n"
        "  python scripts/main_loop.py finish -plan <path> -status <status> "
        "-progress <file> [-note <text>] [-verified <text>]\n"
        "  python scripts/main_loop.py status -plans <dir> -progress <file>\n"
        "  python scripts/main_loop.py -h\n"
        "Options:\n"
        "  -plans DIR\n"
        "  -plan PATH\n"
        "  -status done|blocked|skipped\n"
        "  -progress FILE\n"
        "  -note TEXT\n"
        "  -owner NAME\n"
        "  -verified TEXT\n"
        "  -h, -help Show this help.\n"
    )


def parse_flags(args: list[str]) -> dict[str, str]:
    flags: dict[str, str] = {}
    idx = 0
    while idx < len(args):
        arg = args[idx]
        if arg in ("-h", "-help"):
            raise ValueError("help")
        if not arg.startswith("-"):
            raise ValueError(f"unexpected arg: {arg}")
        if idx + 1 >= len(args):
            raise ValueError(f"missing value for {arg}")
        flags[arg] = args[idx + 1]
        idx += 2
    return flags


def normalize_plan_key(plan_value: str) -> str:
    raw = plan_value.strip().replace("\\", "/")
    raw = raw.lstrip("./")
    if raw.startswith("docs/superpowers/plans/"):
        return raw[len("docs/superpowers/plans/") :]
    marker = "/docs/superpowers/plans/"
    if marker in raw:
        return raw.split(marker, 1)[1]
    return raw


def normalize_note(note: str) -> str:
    return note.replace("\n", " ").replace("\r", " ").replace("`", "'").strip()


def now_utc() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace(
        "+00:00", "Z"
    )


def default_claim_owner() -> str:
    owner = os.environ.get("PLAYBOOK_MAIN_LOOP_OWNER")
    if owner:
        return normalize_note(owner)
    host = socket.gethostname() or "unknown-host"
    try:
        user = getpass.getuser()
    except Exception:  # pragma: no cover
        user = "unknown-user"
    return f"{user}@{host}:{os.getpid()}"


def render_plan_line(plan_key: str, status: str, note: Optional[str]) -> str:
    checked = "x" if status == "done" else " "
    suffix = status
    if note:
        suffix += f": {note}"
    return f"- [{checked}] `{plan_key}` {suffix}"


def list_plan_files(plans_dir: Path) -> list[str]:
    entries: list[str] = []
    for path in plans_dir.iterdir():
        if not path.is_file():
            continue
        if not PLAN_FILE_RE.match(path.name):
            continue
        entries.append(path.name)
    return sorted(entries)


def find_block(lines: list[str]) -> Optional[tuple[int, int]]:
    start_idx = None
    for idx, line in enumerate(lines):
        if line.strip() == PLAN_STATUS_START:
            start_idx = idx
            break
    if start_idx is None:
        return None
    for idx in range(start_idx + 1, len(lines)):
        if lines[idx].strip() == PLAN_STATUS_END:
            return start_idx, idx
    return None


def find_named_block(
    lines: list[str], start_marker: str, end_marker: str
) -> Optional[tuple[int, int]]:
    start_idx = None
    for idx, line in enumerate(lines):
        if line.strip() == start_marker:
            start_idx = idx
            break
    if start_idx is None:
        return None
    for idx in range(start_idx + 1, len(lines)):
        if lines[idx].strip() == end_marker:
            return start_idx, idx
    return None


def parse_entries(
    lines: list[str], start_idx: int, end_idx: int
) -> list[tuple[str, str, Optional[str], int]]:
    entries: list[tuple[str, str, Optional[str], int]] = []
    for idx in range(start_idx + 1, end_idx):
        line = lines[idx].strip()
        match = PLAN_LINE_RE.match(line)
        if not match:
            continue
        plan_key = normalize_plan_key(match.group("plan"))
        status = match.group("status")
        note = match.group("note")
        entries.append((plan_key, status, note, idx))
    return entries


def render_progress_lines(plans: list[str]) -> list[str]:
    lines = [
        "# 当前进展",
        "",
        "## Plan Status",
        "",
        PLAN_STATUS_START,
    ]
    for plan_key in plans:
        lines.append(render_plan_line(plan_key, "pending", None))
    lines.append(PLAN_STATUS_END)
    return lines


def parse_env_blocked_note(note: Optional[str]) -> Optional[tuple[str, str]]:
    if not note:
        return None
    match = ENV_BLOCKED_RE.match(note)
    if not match:
        return None
    return match.group(1), match.group(2)


def detect_env() -> Optional[str]:
    mapping = {"windows": "windows", "linux": "linux", "darwin": "darwin"}
    return mapping.get(platform.system().lower())


def load_progress_lines(progress_path: Path) -> list[str]:
    progress_path.parent.mkdir(parents=True, exist_ok=True)
    if progress_path.exists():
        return progress_path.read_text(encoding="utf-8").splitlines()
    return []


def write_progress_lines(progress_path: Path, lines: list[str]) -> None:
    progress_path.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")


def get_thread_lock(lock_path: Path) -> threading.Lock:
    key = str(lock_path.resolve())
    with THREAD_LOCKS_GUARD:
        lock = THREAD_LOCKS.get(key)
        if lock is None:
            lock = threading.Lock()
            THREAD_LOCKS[key] = lock
        return lock


@contextmanager
def locked_progress(progress_path: Path):
    progress_path.parent.mkdir(parents=True, exist_ok=True)
    lock_path = progress_path.with_name(f"{progress_path.name}.lock")
    thread_lock = get_thread_lock(lock_path)
    with thread_lock:
        with lock_path.open("a+b") as lock_file:
            if fcntl is not None:
                fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
            elif msvcrt is not None:  # pragma: no cover
                while True:
                    try:
                        lock_file.seek(0)
                        msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
                        break
                    except OSError:
                        time.sleep(0.05)
            try:
                hold_ms = os.environ.get("PLAYBOOK_MAIN_LOOP_HOLD_LOCK_MS")
                if hold_ms:
                    time.sleep(max(0.0, float(hold_ms) / 1000.0))
                yield
            finally:
                if fcntl is not None:
                    fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
                elif msvcrt is not None:  # pragma: no cover
                    lock_file.seek(0)
                    msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)


def ensure_section(lines: list[str], heading: str) -> list[str]:
    if any(line.strip() == heading for line in lines):
        return lines
    if lines and lines[-1] != "":
        lines.append("")
    lines.extend([heading, ""])
    return lines


def ensure_block_with_lines(
    lines: list[str],
    start_marker: str,
    end_marker: str,
    default_lines: list[str],
    heading: Optional[str] = None,
) -> tuple[list[str], int, int]:
    block = find_named_block(lines, start_marker, end_marker)
    if block:
        return lines, block[0], block[1]

    if not lines:
        lines = ["# 当前进展", ""]
    if heading:
        lines = ensure_section(lines, heading)
    if lines and lines[-1] != "":
        lines.append("")
    insert_at = len(lines)
    lines[insert_at:insert_at] = default_lines
    return lines, insert_at, insert_at + len(default_lines) - 1


def ensure_plan_block(
    lines: list[str], progress_path: Path, plan_keys: list[str]
) -> tuple[list[str], int, int]:
    lines = remove_workflow_state_block(lines)
    lines, start_idx, end_idx = ensure_block_with_lines(
        lines,
        PLAN_STATUS_START,
        PLAN_STATUS_END,
        [PLAN_STATUS_START, PLAN_STATUS_END],
        "## Plan Status",
    )
    write_progress_lines(progress_path, lines)
    return lines, start_idx, end_idx


def remove_workflow_state_block(lines: list[str]) -> list[str]:
    block = find_named_block(lines, WORKFLOW_STATE_START, WORKFLOW_STATE_END)
    if not block:
        return lines

    start_idx, end_idx = block
    remove_start = start_idx
    if (
        remove_start >= 2
        and lines[remove_start - 1].strip() == ""
        and lines[remove_start - 2].strip() == "## Workflow State"
    ):
        remove_start -= 2
    if remove_start > 0 and lines[remove_start - 1].strip() == "":
        remove_start -= 1

    remove_end = end_idx + 1
    if remove_end < len(lines) and lines[remove_end].strip() == "":
        remove_end += 1
    del lines[remove_start:remove_end]
    return lines


def ensure_all_plans_present(
    lines: list[str],
    start_idx: int,
    end_idx: int,
    progress_path: Path,
    plan_keys: list[str],
) -> list[tuple[str, str, Optional[str], int]]:
    entries = parse_entries(lines, start_idx, end_idx)
    existing = {plan_key for plan_key, _, _, _ in entries}
    missing = [plan_key for plan_key in plan_keys if plan_key not in existing]
    if missing:
        insert_lines = [
            render_plan_line(plan_key, "pending", None) for plan_key in missing
        ]
        lines[end_idx:end_idx] = insert_lines
        write_progress_lines(progress_path, lines)
        end_idx += len(insert_lines)
        entries = parse_entries(lines, start_idx, end_idx)
    return entries


def ensure_plan_status_entry(lines: list[str], plan_key: str) -> list[str]:
    lines = remove_workflow_state_block(lines)
    lines, start_idx, end_idx = ensure_block_with_lines(
        lines,
        PLAN_STATUS_START,
        PLAN_STATUS_END,
        [PLAN_STATUS_START, PLAN_STATUS_END],
        "## Plan Status",
    )
    entries = parse_entries(lines, start_idx, end_idx)
    if plan_key not in {entry_plan for entry_plan, _, _, _ in entries}:
        lines[end_idx:end_idx] = [render_plan_line(plan_key, "pending", None)]
    return lines


def filter_existing_entries(
    entries: list[tuple[str, str, Optional[str], int]], plan_keys: list[str]
) -> list[tuple[str, str, Optional[str], int]]:
    available = set(plan_keys)
    return [entry for entry in entries if entry[0] in available]


def validate_plan_meta(plan_path: Path) -> list[str]:
    text = plan_path.read_text(encoding="utf-8")
    missing: list[str] = []
    if not re.search(r"(?im)^##\s+Plan Meta\s*$", text):
        missing.append("Plan Meta")
    for field in PLAN_META_REQUIRED_FIELDS:
        pattern = rf"(?im)^\s*[-*]\s+(?:\*\*)?{re.escape(field)}(?:\*\*)?\s*:"
        if not re.search(pattern, text):
            missing.append(field)
    return missing


def validate_plan_files(plans_dir: Path, plan_keys: list[str]) -> Optional[str]:
    for plan_key in plan_keys:
        missing = validate_plan_meta(plans_dir / plan_key)
        if missing:
            fields = ", ".join(missing)
            return f"ERROR: {plan_key} missing required Plan Meta fields: {fields}"
    return None


def choose_claim_entry(
    entries: list[tuple[str, str, Optional[str], int]],
    current_env: Optional[str],
    plan_keys: list[str],
) -> Optional[tuple[str, Optional[str], int]]:
    available = set(plan_keys)
    ordered_entries = [entry for entry in entries if entry[0] in available]

    for plan_key, status, note, idx in ordered_entries:
        if status == "in-progress":
            return plan_key, note, idx

    for plan_key, status, note, idx in ordered_entries:
        if status == "pending":
            return plan_key, note, idx
        if status != "blocked" or not current_env:
            continue
        env_info = parse_env_blocked_note(note)
        if env_info and env_info[0] == current_env:
            return plan_key, note, idx

    return None


def claim_plan(
    plans_dir: Path, progress_path: Path, owner: Optional[str] = None
) -> tuple[int, str]:
    if not plans_dir.is_dir():
        return 2, f"ERROR: plans dir not found: {plans_dir}"

    plan_keys = list_plan_files(plans_dir)
    if not plan_keys:
        return 2, "ERROR: no plan files found"
    plan_error = validate_plan_files(plans_dir, plan_keys)
    if plan_error:
        return 2, plan_error

    with locked_progress(progress_path):
        lines = load_progress_lines(progress_path)
        try:
            lines, start_idx, end_idx = ensure_plan_block(
                lines, progress_path, plan_keys
            )
        except ValueError as exc:
            return 2, f"ERROR: {exc}"

        entries = ensure_all_plans_present(
            lines, start_idx, end_idx, progress_path, plan_keys
        )
        entries = filter_existing_entries(entries, plan_keys)
        chosen = choose_claim_entry(entries, detect_env(), plan_keys)
        if not chosen:
            return 0, "NOOP: no claimable plans"

        plan_key, note, idx = chosen
        claim_note = (
            f"claimed_by: {normalize_note(owner) if owner else default_claim_owner()}; "
            f"claimed_at: {now_utc()}"
        )
        updated_note = f"{note}; {claim_note}" if note else claim_note
        lines[idx] = render_plan_line(plan_key, "in-progress", updated_note)
        write_progress_lines(progress_path, lines)

    output = [f"PLAN={(plans_dir / plan_key).as_posix()}"]
    if note:
        output.append(f"NOTE={note}")
    return 0, "\n".join(output)


def finish_plan(
    plan: str,
    status: str,
    progress_path: Path,
    note: Optional[str],
    verified: Optional[str] = None,
) -> tuple[int, str]:
    if status not in FINISH_STATUSES:
        return 2, f"ERROR: invalid status: {status}"
    if not plan:
        return 2, "ERROR: plan is required"
    if verified and status != "done":
        return 2, "ERROR: -verified is only valid with -status done"

    plan_key = normalize_plan_key(plan)
    with locked_progress(progress_path):
        lines = load_progress_lines(progress_path)

        try:
            lines, start_idx, end_idx = ensure_plan_block(
                lines, progress_path, [plan_key]
            )
        except ValueError as exc:
            return 2, f"ERROR: {exc}"

        entries = parse_entries(lines, start_idx, end_idx)
        rendered_note = normalize_note(note) if note else None
        rendered_verified = normalize_note(verified) if verified else None
        if rendered_verified:
            verified_note = f"verified: {rendered_verified}"
            rendered_note = (
                f"{verified_note}; note: {rendered_note}"
                if rendered_note
                else verified_note
            )
        updated_line = render_plan_line(plan_key, status, rendered_note)

        for entry_plan, _, _, idx in entries:
            if entry_plan == plan_key:
                lines[idx] = updated_line
                write_progress_lines(progress_path, lines)
                return 0, updated_line

        lines[end_idx:end_idx] = [updated_line]
        write_progress_lines(progress_path, lines)
        return 0, updated_line


def status_report(plans_dir: Path, progress_path: Path) -> tuple[int, str]:
    if not plans_dir.is_dir():
        return 2, f"ERROR: plans dir not found: {plans_dir}"

    plan_keys = list_plan_files(plans_dir)
    lines = load_progress_lines(progress_path)
    block = find_block(lines)
    entries: list[tuple[str, str, Optional[str], int]] = []
    if block:
        entries = filter_existing_entries(parse_entries(lines, *block), plan_keys)

    entry_by_plan = {plan_key: (status, note) for plan_key, status, note, _ in entries}
    counts = {status: 0 for status in ("pending", "in-progress", "done", "blocked", "skipped")}
    rows: list[str] = []
    for plan_key in plan_keys:
        status, note = entry_by_plan.get(plan_key, ("pending", None))
        counts[status] += 1
        suffix = f": {note}" if note else ""
        rows.append(f"PLAN {plan_key} {status}{suffix}")

    output = [
        "STATUS "
        f"total={len(plan_keys)} "
        f"pending={counts['pending']} "
        f"in-progress={counts['in-progress']} "
        f"done={counts['done']} "
        f"blocked={counts['blocked']} "
        f"skipped={counts['skipped']}"
    ]
    output.extend(rows)
    return 0, "\n".join(output)


def record_plan(progress_path: Path, plan: str) -> tuple[int, str]:
    with locked_progress(progress_path):
        lines = load_progress_lines(progress_path)
        lines = ensure_plan_status_entry(lines, normalize_plan_key(plan))
        write_progress_lines(progress_path, lines)
    return 0, "OK"


def main(argv: list[str]) -> int:
    if not argv:
        print(usage(), file=sys.stderr)
        return 2
    if argv[0] in ("-h", "-help"):
        print(usage())
        return 0

    mode = argv[0]
    if mode not in {"claim", "finish", "status"}:
        print(f"ERROR: unknown mode: {mode}", file=sys.stderr)
        print(usage(), file=sys.stderr)
        return 2

    try:
        flags = parse_flags(argv[1:])
    except ValueError as exc:
        if str(exc) == "help":
            print(usage())
            return 0
        print(f"ERROR: {exc}", file=sys.stderr)
        print(usage(), file=sys.stderr)
        return 2

    if mode == "claim":
        plans = flags.get("-plans")
        progress = flags.get("-progress")
        owner = flags.get("-owner")
        if not plans or not progress:
            print("ERROR: -plans and -progress are required", file=sys.stderr)
            print(usage(), file=sys.stderr)
            return 2
        code, message = claim_plan(Path(plans), Path(progress), owner)
        if code != 0:
            print(message, file=sys.stderr)
            return code
        print(message)
        return 0

    if mode == "status":
        plans = flags.get("-plans")
        progress = flags.get("-progress")
        if not plans or not progress:
            print("ERROR: -plans and -progress are required", file=sys.stderr)
            print(usage(), file=sys.stderr)
            return 2
        code, message = status_report(Path(plans), Path(progress))
        if code != 0:
            print(message, file=sys.stderr)
            return code
        print(message)
        return 0

    plan = flags.get("-plan")
    status = flags.get("-status")
    progress = flags.get("-progress")
    note = flags.get("-note")
    verified = flags.get("-verified")
    if not plan or not status or not progress:
        print("ERROR: -plan, -status, and -progress are required", file=sys.stderr)
        print(usage(), file=sys.stderr)
        return 2
    code, message = finish_plan(plan, status, Path(progress), note, verified)
    if code != 0:
        print(message, file=sys.stderr)
        return code
    print(message)
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
