#!/usr/bin/env python3
"""Download issue data referenced by the Feishu issue export JSON."""

from __future__ import annotations

import argparse
import json
import re
import shutil
import subprocess
import sys
from collections import OrderedDict
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Iterable

FILE = Path(__file__).resolve()
ROOT = FILE.parents[2]
if str(ROOT) not in sys.path:
    sys.path.append(str(ROOT))

from tools.feishu_project.case_calib_recovery import recover_camera4_json


PDCL_REF_RE = re.compile(r"ADAS_[^:/\\\s]+::[^/\\\s]*")
MDI_RAW_REF_ARG_RE = re.compile(r"(?:^|\s)mdi\s+raw\b.*?(?:^|\s)-r\s+([^\s]+)")
STANDARD_PATH_SPLIT_RE = re.compile(r"[，,\n;；]+")
SHARED_CALIB_REL = Path("test_data") / "calibs" / "camera4.json"
PLACEHOLDER_TEXTS = {"待填", "待补充", "none", "null", "待提供"}
NETWORK_SHARE_PREFIX_MAPPINGS = (
    ("//hfs.minieye.tech/project-D4Q2", "/mnt/D4Q2"),
    ("//192.168.2.122/project-D4Q2", "/mnt/D4Q2"),
    ("//hfs.minieye.tech/project-G1M3", "/mnt/G1M3"),
    ("//192.168.2.122/project-G1M3", "/mnt/G1M3"),
    ("//hfs.minieye.tech/G1M3", "/mnt/G1M3"),
    ("//192.168.2.122/G1M3", "/mnt/G1M3"),
)


@dataclass
class ActionResult:
    issue_id: int
    issue_name: str
    source_field: str
    source_kind: str
    raw_value: str | None
    normalized_ref: str | None
    output_dir: str
    status: str
    detail: str
    resolved_source_path: str | None = None
    command: list[str] | None = None
    candidate_paths: list[str] | None = None
    selected_subpath: str | None = None

    def to_dict(self) -> dict:
        return {
            "issue_id": self.issue_id,
            "issue_name": self.issue_name,
            "source_field": self.source_field,
            "source_kind": self.source_kind,
            "raw_value": self.raw_value,
            "normalized_ref": self.normalized_ref,
            "output_dir": self.output_dir,
            "status": self.status,
            "detail": self.detail,
            "resolved_source_path": self.resolved_source_path,
            "command": self.command,
            "candidate_paths": self.candidate_paths,
            "selected_subpath": self.selected_subpath,
        }


@dataclass(frozen=True)
class PDCLDownloadRequest:
    normalized_ref: str
    selected_subpath: str | None = None
    raw_token: str | None = None


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Download or stage issue data from a Feishu issue export JSON."
    )
    parser.add_argument(
        "--input-json",
        default="tools/feishu_project/dongying_g1q3_issue_list.json",
        help="Path to the issue export JSON.",
    )
    parser.add_argument(
        "--output-root",
        required=True,
        help="Directory where downloaded or copied data should be stored.",
    )
    parser.add_argument(
        "--manifest-path",
        default=None,
        help="Optional explicit path for the execution manifest JSON.",
    )
    parser.add_argument(
        "--issue-id",
        action="append",
        dest="issue_ids",
        type=int,
        help="Optional issue id filter. Can be repeated.",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Plan actions without running mdi or copying files.",
    )
    parser.add_argument(
        "--skip-mdi",
        action="store_true",
        help="Skip PDCL/MDI downloads and only process standard paths.",
    )
    parser.add_argument(
        "--skip-copy",
        action="store_true",
        help="Skip standard-path copies and only process PDCL/MDI downloads.",
    )
    parser.add_argument(
        "--only-redownload-affected-cases",
        action="store_true",
        help=(
            "Only re-copy standard-path cases affected by the historical sigmastar.1/camera4.bin "
            "copy bug. This mode skips PDCL/MDI downloads and replaces stale copied targets."
        ),
    )
    parser.add_argument(
        "--skip-calib-recovery",
        action="store_true",
        help=(
            "Skip recovering camera4.json from camera_config_folder.bin or mcap attachments "
            "after standard-path copies."
        ),
    )
    return parser.parse_args()


def load_issue_items(path: Path) -> list[dict]:
    payload = json.loads(path.read_text(encoding="utf-8"))
    return payload["items"]


def ensure_dir(path: Path, dry_run: bool) -> None:
    if dry_run:
        return
    path.mkdir(parents=True, exist_ok=True)


def log_progress(message: str) -> None:
    timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S")
    print(f"[download_issue_data {timestamp}] {message}", flush=True)


def compact_text(value: object, max_len: int = 96) -> str:
    text = "" if value is None else str(value).strip()
    text = re.sub(r"\s+", " ", text)
    if len(text) <= max_len:
        return text
    return f"{text[: max_len - 3]}..."


def summarize_issue_results(results: list[ActionResult]) -> str:
    if not results:
        return "no actions"
    summary: dict[str, int] = {}
    for result in results:
        summary[result.status] = summary.get(result.status, 0) + 1
    return ", ".join(f"{status}={summary[status]}" for status in sorted(summary))


def normalize_issue_dirname(issue_id: int) -> str:
    return f"issue_{issue_id}"


def iter_issue_fields(item: dict) -> Iterable[tuple[str, object]]:
    yield "问题数据地址", item.get("问题数据地址")
    yield "问题数据地址_PDCL", item.get("问题数据地址_PDCL")


def _normalize_pdcl_selected_subpath(raw_subpath: str) -> str | None:
    cleaned = raw_subpath.strip().strip("/")
    if not cleaned:
        return None
    candidate = Path(cleaned)
    if candidate.is_absolute():
        return None
    if any(part in {"", ".", ".."} for part in candidate.parts):
        return None
    return str(candidate)


def _build_pdcl_request_from_token(token: str) -> PDCLDownloadRequest | None:
    stripped = token.strip().strip("\"'`")
    match = PDCL_REF_RE.match(stripped)
    if match is None:
        return None
    normalized_ref = match.group(0)
    suffix = stripped[match.end():]
    selected_subpath = None
    if suffix.startswith("/"):
        raw_subpath = suffix[1:]
        if raw_subpath and not raw_subpath.startswith("ADAS_"):
            selected_subpath = _normalize_pdcl_selected_subpath(raw_subpath)
    return PDCLDownloadRequest(
        normalized_ref=normalized_ref,
        selected_subpath=selected_subpath,
        raw_token=stripped,
    )


def extract_pdcl_requests(raw_value: object) -> list[PDCLDownloadRequest]:
    if raw_value is None:
        return []
    text = str(raw_value).strip()
    if not text:
        return []
    requests: list[PDCLDownloadRequest] = []

    for segment in (part.strip() for part in STANDARD_PATH_SPLIT_RE.split(text)):
        if not segment:
            continue

        mdi_match = MDI_RAW_REF_ARG_RE.search(segment)
        if mdi_match is not None:
            request = _build_pdcl_request_from_token(mdi_match.group(1))
            if request is not None:
                requests.append(request)
            continue

        search_pos = 0
        while True:
            match = PDCL_REF_RE.search(segment, search_pos)
            if match is None:
                break
            normalized_ref = match.group(0)
            suffix = segment[match.end():]
            selected_subpath = None
            if suffix.startswith("/") and not suffix[1:].startswith("ADAS_"):
                selected_subpath = _normalize_pdcl_selected_subpath(suffix[1:])
                requests.append(
                    PDCLDownloadRequest(
                        normalized_ref=normalized_ref,
                        selected_subpath=selected_subpath,
                        raw_token=segment,
                    )
                )
                break

            requests.append(
                PDCLDownloadRequest(
                    normalized_ref=normalized_ref,
                    selected_subpath=None,
                    raw_token=normalized_ref,
                )
            )
            search_pos = match.end()

    deduped: list[PDCLDownloadRequest] = []
    seen = set()
    for request in requests:
        key = (request.normalized_ref, request.selected_subpath)
        if key in seen:
            continue
        seen.add(key)
        deduped.append(request)
    return deduped


def extract_standard_paths(raw_value: object) -> list[str]:
    if raw_value is None:
        return []
    text = str(raw_value).strip()
    if not text:
        return []
    if text.lower() in PLACEHOLDER_TEXTS:
        return []
    if extract_pdcl_requests(text):
        return []
    if "/" not in text and "\\" not in text:
        return []
    parts = [part.strip() for part in STANDARD_PATH_SPLIT_RE.split(text)]
    return [part for part in parts if part]


def normalize_standard_source_path(path: Path) -> Path:
    normalized = path
    if normalized.name == "camera4.bin" and normalized.parent.name == "sigmastar.1":
        return normalized.parent.parent
    if normalized.name == "sigmastar.1":
        return normalized.parent
    return normalized


def is_affected_standard_path(raw_path: str) -> bool:
    raw_path_obj = Path(raw_path.strip())
    return normalize_standard_source_path(raw_path_obj) != raw_path_obj


def normalize_share_path_separators(path_str: str) -> str:
    normalized = path_str.strip().replace("\\", "/")
    normalized = re.sub(r"/{3,}", "//", normalized)
    return normalized


def rewrite_network_share_path(path_str: str) -> str | None:
    normalized = normalize_share_path_separators(path_str)
    for prefix_src, prefix_dst in NETWORK_SHARE_PREFIX_MAPPINGS:
        if normalized.startswith(prefix_src):
            return f"{prefix_dst}{normalized[len(prefix_src):]}"
    return None


def build_path_candidates(raw_path: str) -> list[Path]:
    candidates: list[str] = [raw_path]

    normalized_share = normalize_share_path_separators(raw_path)
    if normalized_share != raw_path:
        candidates.append(normalized_share)

    network_share_rewritten = rewrite_network_share_path(raw_path)
    if network_share_rewritten:
        candidates.append(network_share_rewritten)

    for needle in ("hfs/project-G1M3", "project-G1M3"):
        for candidate in list(candidates):
            if needle in candidate:
                candidates.append(candidate.replace(needle, "G1M3"))

    normalized_candidates = [normalize_standard_source_path(Path(candidate)) for candidate in candidates]
    unique_candidates = list(OrderedDict.fromkeys(str(candidate) for candidate in normalized_candidates))
    return [Path(candidate) for candidate in unique_candidates]


def resolve_existing_path(raw_path: str) -> tuple[Path | None, list[Path]]:
    candidates = build_path_candidates(raw_path)
    for candidate in candidates:
        if candidate.exists():
            return candidate, candidates
    return None, candidates


def remove_existing_target(path: Path) -> None:
    if path.is_dir():
        shutil.rmtree(path)
    else:
        path.unlink()


def copy_source_path(
    source_path: Path,
    output_dir: Path,
    dry_run: bool,
    replace_existing: bool = False,
    legacy_target_names: Iterable[str] | None = None,
) -> tuple[str, str]:
    target = output_dir / source_path.name
    cleanup_targets: list[Path] = [target]
    if legacy_target_names:
        for target_name in legacy_target_names:
            cleanup_targets.append(output_dir / target_name)

    unique_cleanup_targets = list(OrderedDict.fromkeys(str(path) for path in cleanup_targets))
    cleanup_paths = [Path(path) for path in unique_cleanup_targets]
    existing_targets = [path for path in cleanup_paths if path.exists()]

    if existing_targets and not replace_existing:
        return "exists", f"target already exists: {target}"

    if dry_run:
        if existing_targets and replace_existing:
            existing_str = ", ".join(str(path) for path in existing_targets)
            return "planned_redownload", f"would replace {existing_str} with {source_path} -> {target}"
        return "planned", f"would copy {source_path} -> {target}"

    ensure_dir(output_dir, dry_run=False)
    if replace_existing:
        for existing_target in existing_targets:
            remove_existing_target(existing_target)

    if source_path.is_dir():
        shutil.copytree(source_path, target)
    else:
        shutil.copy2(source_path, target)
    if existing_targets and replace_existing:
        replaced = ", ".join(str(path) for path in existing_targets)
        return "redownloaded", f"replaced {replaced} with {target}"
    return "copied", f"copied to {target}"


def build_copied_target_root(output_dir: Path, source_path: Path) -> Path:
    return output_dir / source_path.name


def find_shared_test_data_dir(source_path: Path, max_parent_levels: int = 4) -> Path | None:
    if (source_path / SHARED_CALIB_REL).is_file():
        return None

    current = source_path.parent
    for _ in range(max_parent_levels):
        candidate = current / "test_data"
        if (candidate / "calibs" / "camera4.json").is_file():
            return candidate
        if current.parent == current:
            break
        current = current.parent
    return None


def sync_shared_test_data(
    source_path: Path,
    target_root: Path,
    dry_run: bool,
) -> tuple[str | None, str | None, str | None]:
    shared_test_data_dir = find_shared_test_data_dir(source_path)
    if shared_test_data_dir is None:
        return None, None, None

    target_shared_test_data_dir = target_root / "test_data"
    target_shared_calib = target_shared_test_data_dir / "calibs" / "camera4.json"
    if target_shared_calib.is_file():
        return None, None, None

    if dry_run:
        return (
            "planned_shared_calib_sync",
            f"would copy shared test_data {shared_test_data_dir} -> {target_shared_test_data_dir}",
            str(shared_test_data_dir),
        )

    shutil.copytree(shared_test_data_dir, target_shared_test_data_dir, dirs_exist_ok=True)
    return (
        "synced_shared_calib",
        f"copied shared test_data {shared_test_data_dir} -> {target_shared_test_data_dir}",
        str(shared_test_data_dir),
    )


def recover_target_root_calib(
    source_root: Path,
    target_root: Path,
    dry_run: bool,
) -> tuple[str, str, str | None]:
    recovery = recover_camera4_json(
        source_root=source_root,
        target_root=target_root,
        dry_run=dry_run,
    )
    return recovery.status, recovery.detail, None if recovery.source_path is None else str(recovery.source_path)


def _expected_pdcl_root_dir(output_dir: Path, ref: str) -> Path:
    if "::" not in ref:
        raise ValueError(f"Unexpected PDCL ref without '::': {ref}")
    return output_dir / ref.split("::", 1)[1]


def _prune_pdcl_download_to_selected_subpath(
    root_dir: Path,
    selected_subpath: str,
    dry_run: bool,
) -> tuple[str, str]:
    selected_rel = Path(selected_subpath)
    selected_path = root_dir / selected_rel
    if dry_run:
        return (
            "planned_selected_subpath",
            f"would keep {selected_path} and shared test_data under {root_dir}",
        )

    if not selected_path.exists():
        return (
            "failed_selected_subpath_missing",
            f"selected subpath not found after mdi download: {selected_path}",
        )

    keep_names = {selected_rel.parts[0], "test_data"}
    removed_children: list[str] = []

    for child in root_dir.iterdir():
        if child.name in keep_names:
            continue
        removed_children.append(child.name)
        remove_existing_target(child)

    detail = f"kept selected subpath {selected_path}"
    if removed_children:
        detail += f"; removed siblings: {', '.join(sorted(removed_children))}"
    return "downloaded_selected_subpath", detail


def run_mdi_download(request: PDCLDownloadRequest, output_dir: Path, dry_run: bool) -> tuple[str, str, list[str]]:
    command = ["mdi", "raw", "-r", request.normalized_ref, "-s", str(output_dir)]
    if dry_run:
        if request.selected_subpath:
            root_dir = _expected_pdcl_root_dir(output_dir, request.normalized_ref)
            status, detail = _prune_pdcl_download_to_selected_subpath(root_dir, request.selected_subpath, dry_run=True)
            return status, f"would run {' '.join(command)}; {detail}", command
        return "planned", f"would run {' '.join(command)}", command

    ensure_dir(output_dir, dry_run=False)
    completed = subprocess.run(
        command,
        check=False,
        capture_output=True,
        text=True,
        encoding="utf-8",
    )
    if completed.returncode == 0:
        base_detail = completed.stdout.strip() or "mdi raw completed"
        if request.selected_subpath:
            root_dir = _expected_pdcl_root_dir(output_dir, request.normalized_ref)
            status, detail = _prune_pdcl_download_to_selected_subpath(root_dir, request.selected_subpath, dry_run=False)
            return status, f"{base_detail}\n{detail}", command
        return "downloaded", base_detail, command

    detail = completed.stderr.strip() or completed.stdout.strip() or "mdi raw failed"
    return "failed", detail, command


def process_issue(
    item: dict,
    output_root: Path,
    dry_run: bool,
    skip_mdi: bool,
    skip_copy: bool,
    only_redownload_affected_cases: bool,
    skip_calib_recovery: bool,
) -> list[ActionResult]:
    issue_id = int(item["id"])
    issue_name = str(item["name"])
    issue_dir = output_root / normalize_issue_dirname(issue_id)

    results: list[ActionResult] = []
    seen_pdcl_requests: set[tuple[str, str | None]] = set()
    seen_paths: set[str] = set()
    pdcl_index = 0
    path_index = 0

    for field_name, raw_value in iter_issue_fields(item):
        if not skip_mdi and not only_redownload_affected_cases:
            for request in extract_pdcl_requests(raw_value):
                request_key = (request.normalized_ref, request.selected_subpath)
                if request_key in seen_pdcl_requests:
                    results.append(
                        ActionResult(
                            issue_id=issue_id,
                            issue_name=issue_name,
                            source_field=field_name,
                            source_kind="pdcl_mdi_download",
                            raw_value=None if raw_value is None else str(raw_value),
                            normalized_ref=request.normalized_ref,
                            output_dir=str(issue_dir),
                            status="skipped_duplicate",
                            detail=(
                                f"duplicate PDCL ref: {request.normalized_ref}"
                                if request.selected_subpath is None
                                else f"duplicate PDCL ref+subpath: {request.normalized_ref} / {request.selected_subpath}"
                            ),
                            selected_subpath=request.selected_subpath,
                        )
                    )
                    continue

                seen_pdcl_requests.add(request_key)
                pdcl_index += 1
                download_dir = issue_dir / f"pdcl_{pdcl_index:02d}"
                request_desc = request.normalized_ref
                if request.selected_subpath:
                    request_desc += f"/{request.selected_subpath}"
                log_progress(
                    f"issue_{issue_id} [download] pdcl_{pdcl_index:02d}: {compact_text(request_desc)}"
                )
                status, detail, command = run_mdi_download(request, download_dir, dry_run=dry_run)
                results.append(
                    ActionResult(
                        issue_id=issue_id,
                        issue_name=issue_name,
                        source_field=field_name,
                        source_kind="pdcl_mdi_download",
                        raw_value=None if raw_value is None else str(raw_value),
                        normalized_ref=request.normalized_ref,
                        output_dir=str(download_dir),
                        status=status,
                        detail=detail,
                        command=command,
                        selected_subpath=request.selected_subpath,
                    )
                )

        if skip_copy:
            continue

        for raw_path in extract_standard_paths(raw_value):
            if raw_path in seen_paths:
                results.append(
                    ActionResult(
                        issue_id=issue_id,
                        issue_name=issue_name,
                        source_field=field_name,
                        source_kind="standard_path",
                        raw_value=raw_path,
                        normalized_ref=None,
                        output_dir=str(issue_dir),
                        status="skipped_duplicate",
                        detail=f"duplicate standard path: {raw_path}",
                    )
                )
                continue

            seen_paths.add(raw_path)
            path_index += 1
            copy_dir = issue_dir / f"path_{path_index:02d}"
            affected_standard_path = is_affected_standard_path(raw_path)
            if only_redownload_affected_cases and not affected_standard_path:
                continue

            log_progress(
                f"issue_{issue_id} [download] path_{path_index:02d}: {compact_text(raw_path)}"
            )
            resolved_source_path, candidates = resolve_existing_path(raw_path)
            if resolved_source_path is None:
                results.append(
                    ActionResult(
                        issue_id=issue_id,
                        issue_name=issue_name,
                        source_field=field_name,
                        source_kind="standard_path",
                        raw_value=raw_path,
                        normalized_ref=None,
                        output_dir=str(copy_dir),
                        status="skipped_missing",
                        detail="source path not found after rewrite attempts",
                        candidate_paths=[str(candidate) for candidate in candidates],
                    )
                )
                continue

            legacy_target_names = []
            if affected_standard_path:
                legacy_target_names.append(Path(raw_path.strip()).name)

            status, detail = copy_source_path(
                resolved_source_path,
                copy_dir,
                dry_run=dry_run,
                replace_existing=only_redownload_affected_cases and affected_standard_path,
                legacy_target_names=legacy_target_names,
            )
            target_root = build_copied_target_root(copy_dir, resolved_source_path)
            results.append(
                ActionResult(
                    issue_id=issue_id,
                    issue_name=issue_name,
                    source_field=field_name,
                    source_kind="standard_path",
                    raw_value=raw_path,
                    normalized_ref=None,
                    output_dir=str(copy_dir),
                    status=status,
                    detail=detail,
                    resolved_source_path=str(resolved_source_path),
                    candidate_paths=[str(candidate) for candidate in candidates],
                )
            )

            sync_status, sync_detail, shared_source_dir = sync_shared_test_data(
                resolved_source_path,
                target_root,
                dry_run=dry_run,
            )
            if sync_status is not None:
                results.append(
                    ActionResult(
                        issue_id=issue_id,
                        issue_name=issue_name,
                        source_field=field_name,
                        source_kind="shared_test_data",
                        raw_value=raw_path,
                        normalized_ref=None,
                        output_dir=str(target_root / "test_data"),
                        status=sync_status,
                        detail=sync_detail or "",
                        resolved_source_path=shared_source_dir,
                        candidate_paths=[str(candidate) for candidate in candidates],
                    )
                )

            if not skip_calib_recovery:
                calib_status, calib_detail, calib_source_path = recover_target_root_calib(
                    source_root=resolved_source_path,
                    target_root=target_root,
                    dry_run=dry_run,
                )
                results.append(
                    ActionResult(
                        issue_id=issue_id,
                        issue_name=issue_name,
                        source_field=field_name,
                        source_kind="case_calib_recovery",
                        raw_value=raw_path,
                        normalized_ref=None,
                        output_dir=str(target_root / "test_data" / "calibs"),
                        status=calib_status,
                        detail=calib_detail,
                        resolved_source_path=calib_source_path,
                        candidate_paths=[str(candidate) for candidate in candidates],
                    )
                )

    return results


def build_manifest(
    args: argparse.Namespace,
    input_json: Path,
    output_root: Path,
    action_results: list[ActionResult],
) -> dict:
    summary: dict[str, int] = {}
    for action in action_results:
        summary[action.status] = summary.get(action.status, 0) + 1

    return {
        "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
        "input_json": str(input_json),
        "output_root": str(output_root),
        "dry_run": args.dry_run,
        "skip_mdi": args.skip_mdi,
        "skip_copy": args.skip_copy,
        "skip_calib_recovery": args.skip_calib_recovery,
        "only_redownload_affected_cases": args.only_redownload_affected_cases,
        "issue_filter": args.issue_ids or [],
        "summary": summary,
        "actions": [action.to_dict() for action in action_results],
    }


def print_summary(manifest: dict) -> None:
    print(f"input_json: {manifest['input_json']}")
    print(f"output_root: {manifest['output_root']}")
    print(f"dry_run: {manifest['dry_run']}")
    for status, count in sorted(manifest["summary"].items()):
        print(f"{status}: {count}")


def main() -> int:
    args = parse_args()
    input_json = Path(args.input_json).resolve()
    output_root = Path(args.output_root).resolve()
    manifest_path = (
        Path(args.manifest_path).resolve()
        if args.manifest_path
        else output_root / "download_manifest.json"
    )

    items = load_issue_items(input_json)
    if args.issue_ids:
        issue_filter = set(args.issue_ids)
        items = [item for item in items if int(item["id"]) in issue_filter]

    ensure_dir(output_root, dry_run=args.dry_run)

    action_results: list[ActionResult] = []
    total_items = len(items)
    log_progress(f"issues_to_process: {total_items}")
    for index, item in enumerate(items, start=1):
        issue_id = int(item["id"])
        issue_name = compact_text(item.get("name"), max_len=64)
        log_progress(f"[{index}/{total_items}] issue_{issue_id} start: {issue_name}")
        issue_results = process_issue(
            item=item,
            output_root=output_root,
            dry_run=args.dry_run,
            skip_mdi=args.skip_mdi,
            skip_copy=args.skip_copy,
            only_redownload_affected_cases=args.only_redownload_affected_cases,
            skip_calib_recovery=args.skip_calib_recovery,
        )
        action_results.extend(issue_results)
        log_progress(
            f"[{index}/{total_items}] issue_{issue_id} done: {summarize_issue_results(issue_results)}"
        )

    manifest = build_manifest(args, input_json, output_root, action_results)
    ensure_dir(manifest_path.parent, dry_run=args.dry_run)
    if not args.dry_run:
        manifest_path.write_text(
            json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )

    print_summary(manifest)
    if args.dry_run:
        print(f"manifest (not written in dry-run): {manifest_path}")
    else:
        print(f"manifest: {manifest_path}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
