from __future__ import annotations

import argparse
import json
import os
import re
import signal
import shlex
import shutil
import subprocess
import sys
import time
from pathlib import Path
from typing import Any

from .client import ApiError, AppraisalForgeClient
from .config import get_config_dir, load_sdk_config, merge_sdk_config
from .orchestrator import run_fill
from .relay_worker import run_worker_forever


LOCAL_WORKER_PID = "relay-worker.pid"
LOCAL_WORKER_META = "relay-worker.meta.json"
LOCAL_WORKER_LOG = "relay-worker.log"
WATCH_INTERVAL_SECONDS = 5


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="appraisal-forge", description="Appraisal Forge API CLI")
    parser.add_argument("--api-url", help="Base API URL")
    parser.add_argument("--token", help="Bearer token")

    sub = parser.add_subparsers(dest="command", required=True)

    create = sub.add_parser("create", help="Create an appraisal")
    create.add_argument("--name", required=True)
    create.add_argument("--subject-address", required=True)

    fields_read = sub.add_parser("fields-read", help="Read appraisal fields")
    fields_read.add_argument("--appraisal-id", type=int, required=True)
    fields_read.add_argument("--prefix")
    fields_read.add_argument("--include-empty", choices=["true", "false"])

    fields_write = sub.add_parser("fields-write", help="Write appraisal fields from JSON file")
    fields_write.add_argument("--appraisal-id", type=int, required=True)
    fields_write.add_argument("--json", required=True, dest="json_path")

    validate = sub.add_parser("validate", help="Validate appraisal")
    validate.add_argument("--appraisal-id", type=int, required=True)

    status = sub.add_parser("status", help="Validation summary")
    status.add_argument("--appraisal-id", type=int, required=True)

    schema = sub.add_parser("schema", help="Read field schema")
    schema.add_argument("--prefix")
    schema.add_argument("--search")
    schema.add_argument("--section")

    upload = sub.add_parser("upload-photo", help="Upload appraisal photo")
    upload.add_argument("--appraisal-id", type=int, required=True)
    upload.add_argument("--field-id", required=True)
    upload.add_argument("--image-path", required=True)
    upload.add_argument("--label")

    wf_ingest = sub.add_parser("ingest-workfile", help="Upload local workfile folder/files to an appraisal and stage CSVs")
    wf_ingest.add_argument("--appraisal-id", type=int, required=True)
    wf_ingest.add_argument("--path", required=True, help="Local file or folder path")
    wf_ingest.add_argument("--upload-all", action="store_true", help="Upload all discovered files (default: true)")
    wf_ingest.add_argument("--no-upload-all", action="store_true", help="Only stage selected CSV roles")
    wf_ingest.add_argument("--subject-csv", help="Explicit subject CSV path or file name")
    wf_ingest.add_argument("--comps-csv", help="Explicit comps CSV path or file name")
    wf_ingest.add_argument("--neighborhood-csv", help="Explicit neighborhood CSV path or file name")
    wf_ingest.add_argument("--competing-csv", help="Explicit competing CSV path or file name")
    wf_ingest.add_argument("--dry-run", action="store_true", help="Scan only; do not upload")

    fill = sub.add_parser("fill", help="Run config-driven fill workflow")
    fill.add_argument("--config", required=True, dest="config_path")
    fill.add_argument("--appraisal-id", type=int)
    fill.add_argument("--dry-run", action="store_true")

    sub.add_parser("mcp-serve", help="Start local MCP server (stdio transport)")

    sub.add_parser("list", help="List appraisals")

    get = sub.add_parser("get", help="Get appraisal detail")
    get.add_argument("--appraisal-id", type=int, required=True)

    sub.add_parser("sections", help="List sections")

    sub.add_parser("tokens", help="List API tokens")

    create_token = sub.add_parser("create-token", help="Create a new API token")
    create_token.add_argument("--label", default="Appraisal Forge SDK")
    create_token.add_argument("--scope", default="sdk", choices=["full", "sdk"])

    revoke_token = sub.add_parser("revoke-token", help="Revoke an API token")
    revoke_token.add_argument("--token-id", type=int, required=True)

    sub.add_parser("connection-info", help="Show API and MCP connection details")
    sub.add_parser("verify-token", help="Verify active token can authenticate")
    sub.add_parser("relay-status", help="Relay worker + runtime status")

    login = sub.add_parser("login", help="Device login and local token save")
    login.add_argument("--scope", default="full", choices=["full", "sdk"])
    login.add_argument("--label", default="CLI Device Login")
    login.add_argument("--client-name", default="appraisal-forge-sdk")
    login.add_argument("--timeout-seconds", type=int, default=300)

    relay_start = sub.add_parser("relay-start", help="Start local relay runtime")
    relay_start.add_argument("--agent-cmd", required=True)
    relay_start.add_argument("--appraisal-id", type=int)
    relay_start.add_argument("--base-url")
    relay_start.add_argument("--mode", choices=["local", "server"], default="local")

    relay_stop = sub.add_parser("relay-stop", help="Stop relay runtime")
    relay_stop.add_argument("--mode", choices=["local", "server"], default="local")

    relay_resume = sub.add_parser("relay-resume", help="Resume runtime from saved config")
    relay_resume.add_argument("--agent-cmd")
    relay_resume.add_argument("--appraisal-id", type=int)
    relay_resume.add_argument("--base-url")
    relay_resume.add_argument("--mode", choices=["local", "server"], default="local")

    relay_worker = sub.add_parser("relay-worker", help="Run local relay worker loop in foreground")
    relay_worker.add_argument("--agent-cmd", required=True)
    relay_worker.add_argument("--appraisal-id", type=int)
    relay_worker.add_argument("--poll-wait-seconds", type=int, default=20)
    relay_worker.add_argument("--agent-timeout-seconds", type=int, default=120)

    relay_watch = sub.add_parser("relay-watch", help="Keep local relay worker running (auto-restart loop)")
    relay_watch.add_argument("--agent-cmd")
    relay_watch.add_argument("--appraisal-id", type=int)
    relay_watch.add_argument("--interval-seconds", type=int, default=WATCH_INTERVAL_SECONDS)

    geo = sub.add_parser("geocode", help="Geocode address")
    geo.add_argument("--street", required=True)
    geo.add_argument("--city", required=True)
    geo.add_argument("--state", required=True)
    geo.add_argument("--zip", required=True, dest="zip_code")

    auto_fix = sub.add_parser("auto-fix", help="Auto-fix and validate appraisal")
    auto_fix.add_argument("--appraisal-id", type=int, required=True)

    list_imgs = sub.add_parser("list-images", help="List appraisal images")
    list_imgs.add_argument("--appraisal-id", type=int, required=True)

    del_img = sub.add_parser("delete-image", help="Delete an image")
    del_img.add_argument("--image-id", type=int, required=True)

    adj_create = sub.add_parser("create-adjustment-session", help="Create adjustment session")
    adj_create.add_argument("--appraisal-id", type=int, required=True)

    adj_upload = sub.add_parser("upload-comp-data", help="Upload comp CSV dataset")
    adj_upload.add_argument("--session-id", type=int, required=True)
    adj_upload.add_argument("--csv-path", required=True)

    adj_calc = sub.add_parser("run-calculations", help="Run adjustment calculations")
    adj_calc.add_argument("--session-id", type=int, required=True)

    adj_apply = sub.add_parser("apply-adjustments", help="Apply adjustments to form")
    adj_apply.add_argument("--session-id", type=int, required=True)

    return parser


def _validate_agent_command(agent_cmd: str) -> None:
    cmd = (agent_cmd or "").strip()
    if not cmd:
        raise ValueError("Agent command is required.")
    try:
        parts = shlex.split(cmd)
    except Exception as exc:
        raise ValueError(f"Invalid agent command: {exc}") from exc
    if not parts:
        raise ValueError("Agent command is empty.")
    binary = parts[0]
    if not shutil.which(binary):
        raise ValueError(f"CLI command not found on PATH: {binary}")

    lower = [p.lower() for p in parts]
    if binary == "codex":
        if "--prompt" in lower:
            raise ValueError("Use `codex exec '{message}'` (not `--prompt`).")
        if len(parts) < 2 or parts[1] != "exec":
            raise ValueError("Codex must use non-interactive mode: `codex exec '{message}'`.")
    if binary == "claude":
        if "-p" not in parts and "--print" not in lower:
            raise ValueError("Claude must use non-interactive mode: `claude -p '{message}'`.")
    if binary == "opencode":
        if len(parts) < 2 or parts[1] != "run":
            raise ValueError("OpenCode must use non-interactive mode: `opencode run '{message}'`.")


def _local_worker_pid_path() -> Path:
    return get_config_dir() / LOCAL_WORKER_PID


def _local_worker_meta_path() -> Path:
    return get_config_dir() / LOCAL_WORKER_META


def _local_worker_log_path() -> Path:
    return get_config_dir() / LOCAL_WORKER_LOG


def _read_local_worker_pid() -> int | None:
    path = _local_worker_pid_path()
    if not path.exists():
        return None
    try:
        pid = int(path.read_text(encoding="utf-8").strip())
        return pid if pid > 0 else None
    except Exception:
        return None


def _is_pid_alive(pid: int) -> bool:
    try:
        os.kill(pid, 0)
        return True
    except OSError:
        return False


def _local_worker_status() -> dict[str, Any]:
    pid = _read_local_worker_pid()
    running = bool(pid and _is_pid_alive(pid))
    if pid and not running:
        try:
            _local_worker_pid_path().unlink(missing_ok=True)
        except Exception:
            pass
    meta_path = _local_worker_meta_path()
    meta: dict[str, Any] = {}
    if meta_path.exists():
        try:
            loaded = json.loads(meta_path.read_text(encoding="utf-8"))
            if isinstance(loaded, dict):
                meta = loaded
        except Exception:
            meta = {}
    return {
        "running": running,
        "pid": pid if running else None,
        "agent_cmd": str(meta.get("agent_cmd") or ""),
        "appraisal_id": meta.get("appraisal_id"),
        "started_at": meta.get("started_at"),
        "log_path": str(_local_worker_log_path()),
    }


def _start_local_worker(
    api_url: str,
    token: str,
    agent_cmd: str,
    appraisal_id: int | None = None,
    poll_wait_seconds: int = 20,
    agent_timeout_seconds: int = 120,
) -> dict[str, Any]:
    status = _local_worker_status()
    if status.get("running"):
        _stop_local_worker()

    config_dir = get_config_dir()
    config_dir.mkdir(parents=True, exist_ok=True)
    log_fh = _local_worker_log_path().open("ab", buffering=0)

    cmd = [
        sys.executable,
        "-m",
        "appraisal_forge_sdk",
        "--api-url",
        api_url,
        "--token",
        token,
        "relay-worker",
        "--agent-cmd",
        agent_cmd,
        "--poll-wait-seconds",
        str(max(1, int(poll_wait_seconds))),
        "--agent-timeout-seconds",
        str(max(5, int(agent_timeout_seconds))),
    ]
    if appraisal_id is not None:
        cmd.extend(["--appraisal-id", str(appraisal_id)])

    proc = subprocess.Popen(
        cmd,
        stdin=subprocess.DEVNULL,
        stdout=log_fh,
        stderr=log_fh,
        start_new_session=True,
        cwd=str(Path.cwd()),
    )
    _local_worker_pid_path().write_text(str(proc.pid), encoding="utf-8")
    _local_worker_meta_path().write_text(
        json.dumps(
            {
                "agent_cmd": agent_cmd,
                "appraisal_id": appraisal_id,
                "started_at": int(time.time()),
                "api_url": api_url,
            }
        ),
        encoding="utf-8",
    )
    return {"started": True, **_local_worker_status()}


def _stop_local_worker() -> dict[str, Any]:
    pid = _read_local_worker_pid()
    if not pid:
        return {"stopped": False, **_local_worker_status()}
    try:
        os.kill(pid, signal.SIGTERM)
    except OSError:
        pass
    time.sleep(0.2)
    if _is_pid_alive(pid):
        try:
            os.kill(pid, signal.SIGKILL)
        except OSError:
            pass
    try:
        _local_worker_pid_path().unlink(missing_ok=True)
    except Exception:
        pass
    return {"stopped": True, **_local_worker_status()}


def _require_client(args: argparse.Namespace) -> AppraisalForgeClient:
    config = load_sdk_config()
    api_url = (
        args.api_url
        or os.getenv("APPRAISAL_FORGE_URL")
        or str(config.get("api_url") or "").strip()
    )
    token = (
        args.token
        or os.getenv("APPRAISAL_FORGE_TOKEN")
        or str(config.get("token") or "").strip()
    )
    if not api_url:
        raise ValueError("Missing API URL. Pass --api-url, set APPRAISAL_FORGE_URL, or run `appraisal-forge login`.")
    if not token:
        raise ValueError("Missing token. Pass --token, set APPRAISAL_FORGE_TOKEN, or run `appraisal-forge login`.")

    if args.api_url or args.token:
        merge_sdk_config(api_url=api_url, token=token)
    return AppraisalForgeClient(api_url=api_url, token=token)


def _print_json(data: Any) -> None:
    print(json.dumps(data, indent=2, sort_keys=True))


def _classify_csv_name(name: str) -> str | None:
    s = (name or "").lower()
    if not s.endswith(".csv"):
        return None
    if "subject" in s:
        return "subject"
    if "competing" in s:
        return "competing"
    if "neighborhood" in s:
        return "neighborhood"
    if "rental" in s:
        return "rental"
    if "income" in s:
        return "income"
    if "land" in s:
        return "land"
    if "comp" in s or "sale" in s:
        return "comps"
    return "other"


def _scan_local_files(path_str: str) -> tuple[Path, list[Path], dict[str, list[Path]]]:
    base = Path(path_str).expanduser().resolve()
    if not base.exists():
        raise FileNotFoundError(f"Path not found: {path_str}")

    files: list[Path] = []
    if base.is_file():
        files = [base]
        root = base.parent
    else:
        root = base
        for p in sorted(base.rglob("*")):
            if p.is_file():
                files.append(p)

    by_kind: dict[str, list[Path]] = {}
    for p in files:
        kind = _classify_csv_name(p.name)
        if not kind:
            continue
        by_kind.setdefault(kind, []).append(p)
    return root, files, by_kind


def _resolve_csv_choice(choice: str | None, files: list[Path]) -> Path | None:
    if not choice:
        return None
    c = Path(choice).expanduser()
    if c.is_file():
        return c.resolve()
    needle = choice.strip().lower()
    matches = [p for p in files if p.name.lower() == needle or str(p).lower().endswith(needle)]
    if len(matches) == 1:
        return matches[0]
    return None


def _choose_csv_roles(
    files: list[Path],
    by_kind: dict[str, list[Path]],
    explicit: dict[str, str | None],
) -> tuple[dict[str, Path], dict[str, list[str]], list[str]]:
    roles = ("subject", "comps", "neighborhood", "competing")
    chosen: dict[str, Path] = {}
    ambiguous: dict[str, list[str]] = {}
    missing: list[str] = []

    for role in roles:
        pick = _resolve_csv_choice(explicit.get(role), files)
        if pick:
            chosen[role] = pick
            continue

        cands = by_kind.get(role, [])
        if len(cands) == 1:
            chosen[role] = cands[0]
        elif len(cands) > 1:
            ambiguous[role] = [str(p) for p in cands]
        else:
            missing.append(role)
    return chosen, ambiguous, missing


def _safe_folder_segment(text: str) -> str:
    s = re.sub(r"[^A-Za-z0-9._-]+", "_", (text or "").strip())
    return s[:80] or "bundle"


def _handle(args: argparse.Namespace) -> dict[str, Any]:
    if args.command == "login":
        config = load_sdk_config()
        api_url = (
            args.api_url
            or os.getenv("APPRAISAL_FORGE_URL")
            or str(config.get("api_url") or "").strip()
        )
        if not api_url:
            raise ValueError("Missing API URL. Pass --api-url or set APPRAISAL_FORGE_URL.")

        start = AppraisalForgeClient.start_device_login(
            api_url=api_url,
            scope=args.scope,
            label=args.label,
            client_name=args.client_name,
        )
        device_code = str(start.get("device_code") or "").strip()
        if not device_code:
            raise ValueError("Device login failed: missing device_code in response")

        timeout_seconds = max(15, int(args.timeout_seconds))
        interval = max(1, int(start.get("interval") or 3))
        deadline = time.time() + timeout_seconds

        print("Open this URL in a signed-in browser to approve CLI login:", file=sys.stderr)
        print(start.get("verification_uri_complete") or start.get("verification_uri"), file=sys.stderr)
        print(f"user_code: {start.get('user_code')}", file=sys.stderr)

        token_payload: dict[str, Any] | None = None
        while time.time() < deadline:
            try:
                token_payload = AppraisalForgeClient.poll_device_token(api_url=api_url, device_code=device_code)
                break
            except ApiError as exc:
                code = ""
                if isinstance(exc.payload, dict):
                    code = str(exc.payload.get("error") or "")
                if exc.status_code == 428 or code == "authorization_pending":
                    time.sleep(interval)
                    continue
                raise

        if token_payload is None:
            raise ValueError("Timed out waiting for device approval")

        access_token = str(token_payload.get("access_token") or "").strip()
        if not access_token:
            raise ValueError("Device login completed but response had no access_token")

        saved = merge_sdk_config(
            api_url=api_url,
            token=access_token,
            token_scope=token_payload.get("scope"),
            user=token_payload.get("user"),
        )
        return {
            "ok": True,
            "api_url": saved.get("api_url"),
            "token_saved": True,
            "scope": saved.get("token_scope"),
            "user": saved.get("user"),
        }

    client = _require_client(args)

    if args.command == "create":
        return client.create_appraisal(args.name, args.subject_address)

    if args.command == "fields-read":
        include_empty = None
        if args.include_empty is not None:
            include_empty = args.include_empty == "true"
        return client.read_fields(args.appraisal_id, prefix=args.prefix, include_empty=include_empty)

    if args.command == "fields-write":
        payload = json.loads(Path(args.json_path).read_text())
        if not isinstance(payload, dict):
            raise ValueError("fields JSON must be an object mapping field_id to value")
        return client.write_fields(args.appraisal_id, payload)

    if args.command == "validate":
        return client.validate(args.appraisal_id)

    if args.command == "status":
        data = client.validate(args.appraisal_id)
        summary = {
            "appraisal_id": args.appraisal_id,
            "completeness": data.get("completeness"),
            "critical_errors": len(data.get("critical_errors", [])) if isinstance(data.get("critical_errors"), list) else None,
            "warnings": len(data.get("warnings", [])) if isinstance(data.get("warnings"), list) else None,
        }
        return summary

    if args.command == "schema":
        return client.get_schema(prefix=args.prefix, search=args.search, section=args.section)

    if args.command == "upload-photo":
        return client.upload_image(
            appraisal_id=args.appraisal_id,
            field_id=args.field_id,
            image_path=args.image_path,
            label=args.label,
        )

    if args.command == "ingest-workfile":
        root, files, by_kind = _scan_local_files(args.path)
        explicit = {
            "subject": args.subject_csv,
            "comps": args.comps_csv,
            "neighborhood": args.neighborhood_csv,
            "competing": args.competing_csv,
        }
        chosen, ambiguous, missing = _choose_csv_roles(files, by_kind, explicit)
        upload_all = not bool(args.no_upload_all)
        if args.upload_all:
            upload_all = True

        summary: dict[str, Any] = {
            "ok": True,
            "appraisal_id": args.appraisal_id,
            "path": str(root),
            "scanned_files": len(files),
            "csv_candidates": {k: [str(p) for p in v] for k, v in by_kind.items()},
            "selected_csv_roles": {k: str(v) for k, v in chosen.items()},
            "ambiguous_csv_roles": ambiguous,
            "missing_csv_roles": missing,
            "clarification_required": bool(ambiguous),
            "upload_all": upload_all,
            "dry_run": bool(args.dry_run),
        }
        if args.dry_run:
            return summary

        uploaded: list[dict[str, Any]] = []
        staged: list[dict[str, Any]] = []

        # Upload source bundle for full auditability.
        if upload_all:
            bundle_root = _safe_folder_segment(root.name)
            base_folder = f"additional_data/source_bundle/{bundle_root}"
            for p in files:
                rel_parent = p.relative_to(root).parent
                folder = base_folder if str(rel_parent) == "." else f"{base_folder}/{rel_parent.as_posix()}"
                out = client.upload_workfile(
                    appraisal_id=args.appraisal_id,
                    file_path=str(p),
                    folder=folder,
                )
                uploaded.append(out)

        # Stage canonical CSV roles for agent pipeline.
        for role, p in chosen.items():
            out = client.upload_workfile(
                appraisal_id=args.appraisal_id,
                file_path=str(p),
                folder="additional_data/input_stage",
                remote_name=f"{role}.csv",
            )
            staged.append({"role": role, "source": str(p), "uploaded": out})

        summary["uploaded_files"] = len(uploaded)
        summary["staged_csv_files"] = staged
        summary["next_action"] = (
            "Provide explicit --*-csv for ambiguous roles and rerun ingest-workfile."
            if ambiguous else
            "Run your fill workflow; staged CSVs are ready in additional_data/input_stage."
        )
        return summary

    if args.command == "fill":
        return run_fill(
            client=client,
            config_path=args.config_path,
            appraisal_id=args.appraisal_id,
            dry_run=args.dry_run,
        )

    if args.command == "list":
        return client.list_appraisals()

    if args.command == "get":
        return client.get_appraisal(args.appraisal_id)

    if args.command == "tokens":
        return client.list_tokens()

    if args.command == "create-token":
        return client.create_token(label=args.label, scope=args.scope)

    if args.command == "revoke-token":
        return client.revoke_token(args.token_id)

    if args.command == "connection-info":
        return client.get_connection_info()

    if args.command == "verify-token":
        return client.verify_token()

    if args.command == "relay-status":
        relay = client.relay_status()
        local_worker = _local_worker_status()
        server_runtime = client.relay_runtime_status()
        return {"relay": relay, "local_worker": local_worker, "server_runtime": server_runtime}

    if args.command == "relay-start":
        _validate_agent_command(args.agent_cmd)
        if args.mode == "server":
            result = client.relay_runtime_start(
                agent_cmd=args.agent_cmd,
                appraisal_id=args.appraisal_id,
                base_url=args.base_url,
            )
        else:
            result = _start_local_worker(
                api_url=client.api_url,
                token=client.token,
                agent_cmd=args.agent_cmd,
                appraisal_id=args.appraisal_id,
            )
        merge_sdk_config(
            relay_agent_cmd=args.agent_cmd,
            relay_last_appraisal_id=args.appraisal_id,
            relay_base_url=args.base_url,
        )
        return result

    if args.command == "relay-stop":
        if args.mode == "server":
            return client.relay_runtime_stop()
        return _stop_local_worker()

    if args.command == "relay-resume":
        config = load_sdk_config()
        agent_cmd = args.agent_cmd or str(config.get("relay_agent_cmd") or "").strip()
        if not agent_cmd:
            raise ValueError("Missing relay agent command. Run relay-start once or pass --agent-cmd.")
        _validate_agent_command(agent_cmd)
        appraisal_id = args.appraisal_id
        if appraisal_id is None:
            saved_id = config.get("relay_last_appraisal_id")
            if isinstance(saved_id, int):
                appraisal_id = saved_id
        base_url = args.base_url or str(config.get("relay_base_url") or "").strip() or None
        if args.mode == "server":
            result = client.relay_runtime_start(
                agent_cmd=agent_cmd,
                appraisal_id=appraisal_id,
                base_url=base_url,
            )
        else:
            result = _start_local_worker(
                api_url=client.api_url,
                token=client.token,
                agent_cmd=agent_cmd,
                appraisal_id=appraisal_id,
            )
        merge_sdk_config(
            relay_agent_cmd=agent_cmd,
            relay_last_appraisal_id=appraisal_id,
            relay_base_url=base_url,
        )
        return result

    if args.command == "relay-worker":
        _validate_agent_command(args.agent_cmd)
        run_worker_forever(
            client=client,
            agent_cmd=args.agent_cmd,
            appraisal_id=args.appraisal_id,
            poll_wait_seconds=args.poll_wait_seconds,
            poll_limit=1,
            agent_timeout_seconds=args.agent_timeout_seconds,
        )
        return {"ok": True}

    if args.command == "relay-watch":
        config = load_sdk_config()
        agent_cmd = args.agent_cmd or str(config.get("relay_agent_cmd") or "").strip()
        if not agent_cmd:
            raise ValueError("Missing relay agent command. Run relay-start once or pass --agent-cmd.")
        _validate_agent_command(agent_cmd)
        appraisal_id = args.appraisal_id
        if appraisal_id is None:
            saved_id = config.get("relay_last_appraisal_id")
            if isinstance(saved_id, int):
                appraisal_id = saved_id
        interval = max(1, int(args.interval_seconds or WATCH_INTERVAL_SECONDS))
        while True:
            status = _local_worker_status()
            if not status.get("running"):
                _start_local_worker(
                    api_url=client.api_url,
                    token=client.token,
                    agent_cmd=agent_cmd,
                    appraisal_id=appraisal_id,
                )
            time.sleep(interval)

    if args.command == "sections":
        return client.list_sections()

    if args.command == "geocode":
        return client.geocode(
            street=args.street,
            city=args.city,
            state=args.state,
            zip_code=args.zip_code,
        )

    if args.command == "auto-fix":
        return client.auto_fix(args.appraisal_id)

    if args.command == "list-images":
        return client.list_images(args.appraisal_id)

    if args.command == "delete-image":
        return client.delete_image(args.image_id)

    if args.command == "create-adjustment-session":
        return client.create_adjustment_session(args.appraisal_id)

    if args.command == "upload-comp-data":
        return client.upload_comp_data(
            session_id=args.session_id,
            csv_path=args.csv_path,
        )

    if args.command == "run-calculations":
        return client.run_calculations(args.session_id)

    if args.command == "apply-adjustments":
        return client.apply_adjustments(args.session_id)

    raise ValueError(f"Unsupported command: {args.command}")


def main(argv: list[str] | None = None) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)

    if args.command == "mcp-serve":
        from .mcp_serve import run
        run()
        return 0

    try:
        result = _handle(args)
        _print_json(result)
        return 0
    except (ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 2
    except ApiError as exc:
        print(f"API ERROR: {exc}", file=sys.stderr)
        if exc.payload is not None:
            print(json.dumps({"payload": exc.payload}, indent=2), file=sys.stderr)
        return 1


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