"""generate command: resolve creds → build client → run single pipeline."""

from __future__ import annotations

import json as _json
import logging
import sys
import time
from datetime import UTC, datetime
from pathlib import Path

from aa_auto_sdr.api import fetch
from aa_auto_sdr.api.client import AaClient
from aa_auto_sdr.api.resilience import RetryPolicy
from aa_auto_sdr.core import credentials, timings
from aa_auto_sdr.core.exceptions import (
    AaAutoSdrError,
    AmbiguousMatchError,
    ApiError,
    AuthError,
    ConfigError,
    OutputError,
    ReportSuiteNotFoundError,
)
from aa_auto_sdr.core.exit_codes import ExitCode
from aa_auto_sdr.core.run_summary import PerRsidResult, RunSummary
from aa_auto_sdr.core.version import __version__
from aa_auto_sdr.output import registry
from aa_auto_sdr.pipeline import single
from aa_auto_sdr.sdr.builder import build_sdr

logger = logging.getLogger(__name__)


def _emit_timings_if_enabled(*, show_timings: bool) -> None:
    if not show_timings:
        return
    import sys as _sys

    _sys.stderr.write(timings.format_report())
    _sys.stderr.flush()
    timings.disable()


def _emit_run_summary(
    *,
    run_summary_json: str | None,
    started_at: datetime,
    finished_at: datetime,
    profile: str | None,
    per_rsid: list[PerRsidResult],
    show_timings: bool,
) -> None:
    if not run_summary_json:
        return
    summary = RunSummary(
        started_at=started_at.isoformat(),
        finished_at=finished_at.isoformat(),
        duration_seconds=(finished_at - started_at).total_seconds(),
        tool_version=__version__,
        profile=profile,
        rsids=per_rsid,
        timings=timings.report() if show_timings else [],
    )
    if run_summary_json == "-":
        # Compact single-line JSON on stdout — easier to consume from scripts and
        # plays nicely with `splitlines()[-1]` shaped pipelines.
        payload = _json.dumps(summary.to_dict(), sort_keys=True)
        import sys as _sys

        _sys.stdout.write(payload + "\n")
        _sys.stdout.flush()
    else:
        payload = _json.dumps(summary.to_dict(), sort_keys=True, indent=2)
        Path(run_summary_json).write_text(payload + "\n")
        print(f"wrote run summary: {run_summary_json}", flush=True)


def _emit_pipe_or_print(
    *,
    is_pipe: bool,
    exc: BaseException | None,
    message: str,
    exit_code: int,
) -> None:
    """Pipe-aware error emitter. On pipe path, write JSON envelope to stderr
    (master spec §6.2: pipe-path failures must keep stdout silent so downstream
    `jq` etc. sees empty input); otherwise print the human-readable message to
    stderr, matching the cja_auto_sdr convention that errors never pollute
    stdout."""
    if is_pipe:
        from aa_auto_sdr.output.error_envelope import emit_error_envelope

        # Synthesize an exception when only a string message is available
        # (e.g. format-rejection paths that don't have an exception object).
        emit_error_envelope(exc if exc is not None else RuntimeError(message), exit_code)
    else:
        print(message, file=sys.stderr, flush=True)


def run(
    *,
    rsid: str,
    output_dir: Path,
    format_name: str,
    profile: str | None,
    snapshot: bool = False,
    auto_snapshot: bool = False,
    auto_prune: bool = False,
    keep_last: int | None = None,
    keep_since: str | None = None,
    metrics_only: bool = False,  # v1.2
    dimensions_only: bool = False,  # v1.2
    dry_run: bool = False,  # v1.2 — preview-only; no component fetch, no writes
    open_after: bool = False,  # v1.2 — open first output in OS default app
    assume_yes: bool = False,  # v1.2; accepted for parity, not currently consumed
    show_timings: bool = False,  # v1.2.1
    run_summary_json: str | None = None,  # v1.2.1
    retry_policy: RetryPolicy | None = None,  # v1.7.0 — shared retry budget
    audit_naming: bool = False,  # v1.9.0
    flag_stale: bool = False,  # v1.9.0
    name_match: str = "insensitive",  # v1.9.0
    fail_on_quality: str | None = None,  # v1.12.0
    quality_report: str | None = None,  # v1.12.0
    git_commit: bool = False,  # v1.15.0
    git_push: bool = False,  # v1.15.0
    git_message: str | None = None,  # v1.15.0
    template_path: Path | None = None,  # v1.16.0
    template_organization: str | None = None,  # v1.16.0
    snapshot_dir: Path | None = None,  # resolved by CLI boundary
    notion_force_new: bool = False,  # v1.18.0
    notion_registry_database: str | None = None,  # v1.19.0
    no_notion_registry: bool = False,  # v1.19.0
    notion_company: str | None = None,  # v1.20.0
) -> int:
    """Pattern 9B.1 wrapper: emit command_start/command_complete around the
    real body in ``_run_impl`` so all the existing early returns flow
    through without restructuring."""
    started_ms = time.monotonic()
    logger.info("command_start command=generate", extra={"command": "generate"})
    exit_code = ExitCode.GENERIC.value
    try:
        exit_code = _run_impl(
            rsid=rsid,
            output_dir=output_dir,
            format_name=format_name,
            profile=profile,
            snapshot=snapshot,
            auto_snapshot=auto_snapshot,
            auto_prune=auto_prune,
            keep_last=keep_last,
            keep_since=keep_since,
            metrics_only=metrics_only,
            dimensions_only=dimensions_only,
            dry_run=dry_run,
            open_after=open_after,
            assume_yes=assume_yes,
            show_timings=show_timings,
            run_summary_json=run_summary_json,
            retry_policy=retry_policy,
            audit_naming=audit_naming,
            flag_stale=flag_stale,
            name_match=name_match,
            fail_on_quality=fail_on_quality,
            quality_report=quality_report,
            git_commit=git_commit,
            git_push=git_push,
            git_message=git_message,
            template_path=template_path,
            template_organization=template_organization,
            snapshot_dir=snapshot_dir,
            notion_force_new=notion_force_new,
            notion_registry_database=notion_registry_database,
            no_notion_registry=no_notion_registry,
            notion_company=notion_company,
        )
        return exit_code
    finally:
        duration_ms = int((time.monotonic() - started_ms) * 1000)
        logger.info(
            "command_complete command=generate exit_code=%s duration_ms=%s",
            exit_code,
            duration_ms,
            extra={
                "command": "generate",
                "exit_code": exit_code,
                "duration_ms": duration_ms,
            },
        )


def _run_impl(
    *,
    rsid: str,
    output_dir: Path,
    format_name: str,
    profile: str | None,
    snapshot: bool = False,
    auto_snapshot: bool = False,
    auto_prune: bool = False,
    keep_last: int | None = None,
    keep_since: str | None = None,
    metrics_only: bool = False,
    dimensions_only: bool = False,
    dry_run: bool = False,
    open_after: bool = False,
    assume_yes: bool = False,  # noqa: ARG001 — accepted for parity, not currently consumed
    show_timings: bool = False,
    run_summary_json: str | None = None,
    retry_policy: RetryPolicy | None = None,
    audit_naming: bool = False,  # v1.9.0
    flag_stale: bool = False,  # v1.9.0
    name_match: str = "insensitive",  # v1.9.0
    fail_on_quality: str | None = None,  # v1.12.0
    quality_report: str | None = None,  # v1.12.0
    git_commit: bool = False,  # v1.15.0
    git_push: bool = False,  # v1.15.0
    git_message: str | None = None,  # v1.15.0
    template_path: Path | None = None,  # v1.16.0
    template_organization: str | None = None,  # v1.16.0
    snapshot_dir: Path | None = None,  # resolved by CLI boundary
    notion_force_new: bool = False,  # v1.18.0
    notion_registry_database: str | None = None,  # v1.19.0
    no_notion_registry: bool = False,  # v1.19.0
    notion_company: str | None = None,  # v1.20.0
) -> int:
    started_at = datetime.now(UTC)
    # v1.12.0 — translate string severity to enum once, here.
    from aa_auto_sdr.sdr.quality import SeverityLevel as _SeverityLevel

    _fail_on_quality = _SeverityLevel(fail_on_quality) if fail_on_quality else None

    if show_timings:
        timings.clear()
        timings.enable()

    is_pipe = output_dir == Path("-")

    if metrics_only and dimensions_only:
        msg = "error: --metrics-only and --dimensions-only are mutually exclusive"
        _emit_pipe_or_print(
            is_pipe=is_pipe,
            exc=None,
            message=msg,
            exit_code=ExitCode.USAGE.value,
        )
        return ExitCode.USAGE.value

    if (metrics_only or dimensions_only) and (snapshot or auto_snapshot):
        msg = (
            "error: --metrics-only / --dimensions-only cannot be combined with "
            "--snapshot / --auto-snapshot — filtered snapshots produce misleading diffs"
        )
        _emit_pipe_or_print(
            is_pipe=is_pipe,
            exc=None,
            message=msg,
            exit_code=ExitCode.USAGE.value,
        )
        return ExitCode.USAGE.value

    if is_pipe and quality_report:
        # Pure argument conflict: the quality report is a file artifact and
        # there is no stdout slot for it on the pipe path (stdout carries the
        # SDR JSON). Reject before the credential round-trip so a missing-creds
        # environment cannot mask this with CONFIG (10) instead of OUTPUT (15).
        msg = "error: --quality-report cannot be combined with --output - (use --output-dir <DIR>)"
        _emit_pipe_or_print(is_pipe=is_pipe, exc=None, message=msg, exit_code=ExitCode.OUTPUT.value)
        return ExitCode.OUTPUT.value

    from aa_auto_sdr.sdr.builder import ComponentFilter

    component_filter = ComponentFilter.from_args(
        metrics_only=metrics_only,
        dimensions_only=dimensions_only,
    )

    try:
        creds = credentials.resolve(profile=profile)
    except ConfigError as e:
        _emit_pipe_or_print(is_pipe=is_pipe, exc=e, message=f"error: {e}", exit_code=ExitCode.CONFIG.value)
        return ExitCode.CONFIG.value

    save_required = snapshot or auto_snapshot or git_commit  # --git-commit implies snapshot
    if not save_required:
        snapshot_dir = None

    try:
        formats = registry.resolve_formats(format_name)
    except KeyError as e:
        _emit_pipe_or_print(is_pipe=is_pipe, exc=e, message=f"error: {e}", exit_code=ExitCode.GENERIC.value)
        return ExitCode.GENERIC.value

    if template_path is not None:
        from aa_auto_sdr.output.registry import swap_excel_for_template

        formats = swap_excel_for_template(formats)

    if is_pipe and (len(formats) != 1 or formats[0] != "json"):
        msg = f"error: format {format_name!r} cannot be piped to stdout; use --output-dir <DIR> instead"
        _emit_pipe_or_print(is_pipe=is_pipe, exc=None, message=msg, exit_code=ExitCode.OUTPUT.value)
        return ExitCode.OUTPUT.value

    # Vestigial pre-flight (every concrete format has a writer in v0.2+)
    registry.bootstrap()
    for fmt in formats:
        try:
            registry.get_writer(fmt)
        except KeyError:
            msg = f"error: format '{fmt}' is not available in this build"
            _emit_pipe_or_print(is_pipe=is_pipe, exc=None, message=msg, exit_code=ExitCode.OUTPUT.value)
            return ExitCode.OUTPUT.value

    try:
        with timings.Timer("auth"):
            client = AaClient.from_credentials(creds, retry_policy=retry_policy)
    except AuthError as e:
        _emit_pipe_or_print(is_pipe=is_pipe, exc=e, message=f"auth error: {e}", exit_code=ExitCode.AUTH.value)
        _emit_timings_if_enabled(show_timings=show_timings)
        return ExitCode.AUTH.value

    try:
        with timings.Timer("resolve"):
            canonical_rsids, was_name_lookup = fetch.resolve_rsid(client, rsid, name_match=name_match)
    except AmbiguousMatchError as e:
        print(
            f"error: identifier '{rsid}' is ambiguous; matched {len(e.candidates)} report suites:",
            file=sys.stderr,
        )
        for cand_rsid, cand_name in e.candidates:
            print(f"  - {cand_rsid}  ({cand_name})", file=sys.stderr)
        print(
            "Use a more specific identifier or pass `--name-match exact` (or the rsid directly).",
            file=sys.stderr,
        )
        _emit_timings_if_enabled(show_timings=show_timings)
        return ExitCode.NOT_FOUND.value
    except ReportSuiteNotFoundError as e:
        _emit_pipe_or_print(is_pipe=is_pipe, exc=e, message=f"error: {e}", exit_code=ExitCode.NOT_FOUND.value)
        _emit_timings_if_enabled(show_timings=show_timings)
        return ExitCode.NOT_FOUND.value
    except ApiError as e:
        _emit_pipe_or_print(is_pipe=is_pipe, exc=e, message=f"api error: {e}", exit_code=ExitCode.API.value)
        _emit_timings_if_enabled(show_timings=show_timings)
        return ExitCode.API.value

    # The elif/else prints from earlier versions are removed; rsid_resolved
    # INFO covers them. The multi-name match is still printed for users.
    if not is_pipe and was_name_lookup and len(canonical_rsids) > 1:
        print(
            f"{rsid!r} matches {len(canonical_rsids)} report suites: {', '.join(canonical_rsids)}",
        )
    logger.info(
        "rsid_resolved rsid=%s was_name_lookup=%s",
        canonical_rsids[0],
        was_name_lookup,
        extra={"rsid": canonical_rsids[0]},
    )

    captured_at = datetime.now(UTC)
    total = len(canonical_rsids)

    if dry_run:
        # Preview-only path (v1.2): credentials resolved + auth round-trip done +
        # RSID resolved → list what would be written and exit. No component
        # fetches (the heavy AA calls), no file writes, no snapshot writes.
        print("DRY RUN — would generate:", flush=True)
        ext_map = {
            "excel": "xlsx",
            "excel-template": "xlsx",
            "json": "json",
            "html": "html",
            "markdown": "md",
            "csv": "csv",
        }
        for canonical_rsid in canonical_rsids:
            for fmt in formats:
                if fmt == "csv":
                    # CSV mode produces one file per component type.
                    print(f"  {output_dir / f'{canonical_rsid}.<component>.csv'}")
                elif fmt == "notion":
                    from aa_auto_sdr.output.notion_registry import REGISTRY_FILENAME

                    print(
                        f"  {output_dir / REGISTRY_FILENAME} (Notion page registry; pages are written to Notion, not disk)"
                    )
                else:
                    ext = ext_map.get(fmt, fmt)
                    print(f"  {output_dir / f'{canonical_rsid}.{ext}'}")
            if save_required and snapshot_dir is not None:
                from aa_auto_sdr.snapshot.store import snapshot_path

                snap_path = snapshot_path(
                    snapshot_dir=snapshot_dir,
                    rsid=canonical_rsid,
                    captured_at_iso=captured_at.isoformat(),
                )
                print(f"  {snap_path}")
        print("(no files were written; remove --dry-run to execute)", flush=True)
        dry_per_rsid: list[PerRsidResult] = [
            PerRsidResult(
                rsid=rs,
                name=None,
                succeeded=True,
                formats=list(formats),
                output_paths=[],
                snapshot_path=None,
                error=None,
            )
            for rs in canonical_rsids
        ]
        _emit_run_summary(
            run_summary_json=run_summary_json,
            started_at=started_at,
            finished_at=datetime.now(UTC),
            profile=profile,
            per_rsid=dry_per_rsid,
            show_timings=show_timings,
        )
        _emit_timings_if_enabled(show_timings=show_timings)
        return ExitCode.OK.value

    if is_pipe:
        # JSON-only pipe path: build SdrDocuments and emit one JSON value
        # (single object for one RSID, array of objects for multi-match).
        docs: list[dict] = []
        pipe_per_rsid: list[PerRsidResult] = []
        pipe_gate_failed = False
        from aa_auto_sdr.output.error_envelope import emit_error_envelope

        for canonical_rsid in canonical_rsids:
            try:
                with timings.Timer(f"build:{canonical_rsid}"):
                    doc = build_sdr(
                        client,
                        canonical_rsid,
                        captured_at=captured_at,
                        tool_version=__version__,
                        component_filter=component_filter,
                        audit_naming=audit_naming,
                        flag_stale=flag_stale,
                        fail_on_quality=_fail_on_quality,
                    )
            except ReportSuiteNotFoundError as e:
                emit_error_envelope(e, ExitCode.NOT_FOUND.value)
                pipe_per_rsid.append(
                    PerRsidResult(
                        rsid=canonical_rsid,
                        name=None,
                        succeeded=False,
                        formats=list(formats),
                        output_paths=[],
                        snapshot_path=None,
                        error=str(e),
                    ),
                )
                _emit_run_summary(
                    run_summary_json=run_summary_json,
                    started_at=started_at,
                    finished_at=datetime.now(UTC),
                    profile=profile,
                    per_rsid=pipe_per_rsid,
                    show_timings=show_timings,
                )
                _emit_timings_if_enabled(show_timings=show_timings)
                return ExitCode.NOT_FOUND.value
            except ApiError as e:
                emit_error_envelope(e, ExitCode.API.value)
                pipe_per_rsid.append(
                    PerRsidResult(
                        rsid=canonical_rsid,
                        name=None,
                        succeeded=False,
                        formats=list(formats),
                        output_paths=[],
                        snapshot_path=None,
                        error=str(e),
                    ),
                )
                _emit_run_summary(
                    run_summary_json=run_summary_json,
                    started_at=started_at,
                    finished_at=datetime.now(UTC),
                    profile=profile,
                    per_rsid=pipe_per_rsid,
                    show_timings=show_timings,
                )
                _emit_timings_if_enabled(show_timings=show_timings)
                return ExitCode.API.value
            payload = doc.to_dict()
            docs.append(payload)
            if (
                _fail_on_quality is not None
                and doc.quality is not None
                and doc.quality.get("summary", {}).get("verdict") == "fail"
            ):
                pipe_gate_failed = True
            if snapshot_dir is not None:
                from aa_auto_sdr.snapshot import store as snapshot_store

                try:
                    snapshot_store.save_snapshot(doc, snapshot_dir=snapshot_dir, payload=payload)
                except OutputError as e:
                    emit_error_envelope(e, ExitCode.OUTPUT.value)
                    pipe_per_rsid.append(
                        PerRsidResult(
                            rsid=canonical_rsid,
                            name=doc.report_suite.name,
                            succeeded=False,
                            formats=list(formats),
                            output_paths=[],
                            snapshot_path=None,
                            error=str(e),
                        ),
                    )
                    _emit_run_summary(
                        run_summary_json=run_summary_json,
                        started_at=started_at,
                        finished_at=datetime.now(UTC),
                        profile=profile,
                        per_rsid=pipe_per_rsid,
                        show_timings=show_timings,
                    )
                    _emit_timings_if_enabled(show_timings=show_timings)
                    return ExitCode.OUTPUT.value
            pipe_per_rsid.append(
                PerRsidResult(
                    rsid=canonical_rsid,
                    name=doc.report_suite.name,
                    succeeded=True,
                    formats=list(formats),
                    output_paths=[],
                    snapshot_path=None,
                    error=None,
                ),
            )

        import sys as _sys

        payload = docs[0] if total == 1 else docs
        _sys.stdout.write(_json.dumps(payload, indent=2, default=str) + "\n")
        _sys.stdout.flush()

        if auto_prune and snapshot_dir is not None:
            rc = _apply_auto_prune(
                is_pipe=is_pipe,
                snapshot_dir=snapshot_dir,
                rsids=canonical_rsids,
                keep_last=keep_last,
                keep_since=keep_since,
            )
            if rc != ExitCode.OK.value:
                _emit_run_summary(
                    run_summary_json=run_summary_json,
                    started_at=started_at,
                    finished_at=datetime.now(UTC),
                    profile=profile,
                    per_rsid=pipe_per_rsid,
                    show_timings=show_timings,
                )
                _emit_timings_if_enabled(show_timings=show_timings)
                return rc
        _emit_run_summary(
            run_summary_json=run_summary_json,
            started_at=started_at,
            finished_at=datetime.now(UTC),
            profile=profile,
            per_rsid=pipe_per_rsid,
            show_timings=show_timings,
        )
        _emit_timings_if_enabled(show_timings=show_timings)
        # v1.12.0 quality gate, pipe-path parity: the SDR JSON already streamed
        # to stdout; a failing verdict still turns the exit code into QUALITY
        # exactly like the file-output path below.
        if pipe_gate_failed:
            return ExitCode.QUALITY.value
        return ExitCode.OK.value

    # File-output path: per-RSID pipeline.run_single
    first_output: Path | None = None
    per_rsid_results: list[PerRsidResult] = []
    for index, canonical_rsid in enumerate(canonical_rsids, start=1):
        if total > 1:
            print(f"generating SDR {index}/{total}: {canonical_rsid}")
        try:
            result = single.run_single(
                client=client,
                rsid=canonical_rsid,
                formats=formats,
                output_dir=output_dir,
                captured_at=captured_at,
                tool_version=__version__,
                snapshot_dir=snapshot_dir,
                component_filter=component_filter,
                audit_naming=audit_naming,
                flag_stale=flag_stale,
                fail_on_quality=_fail_on_quality,
                quality_report=quality_report,
                git_commit=git_commit,
                git_push=git_push,
                git_message=git_message,
                template_path=template_path,
                template_organization=template_organization,
                notion_force_new=notion_force_new,
                notion_registry_database=notion_registry_database,
                no_notion_registry=no_notion_registry,
                notion_company=notion_company,
            )
        except ReportSuiteNotFoundError as e:
            print(f"error: {e}", file=sys.stderr, flush=True)
            per_rsid_results.append(
                PerRsidResult(
                    rsid=canonical_rsid,
                    name=None,
                    succeeded=False,
                    formats=list(formats),
                    output_paths=[],
                    snapshot_path=None,
                    error=str(e),
                ),
            )
            _emit_run_summary(
                run_summary_json=run_summary_json,
                started_at=started_at,
                finished_at=datetime.now(UTC),
                profile=profile,
                per_rsid=per_rsid_results,
                show_timings=show_timings,
            )
            _emit_timings_if_enabled(show_timings=show_timings)
            return ExitCode.NOT_FOUND.value
        except ApiError as e:
            print(f"api error: {e}", file=sys.stderr, flush=True)
            per_rsid_results.append(
                PerRsidResult(
                    rsid=canonical_rsid,
                    name=None,
                    succeeded=False,
                    formats=list(formats),
                    output_paths=[],
                    snapshot_path=None,
                    error=str(e),
                ),
            )
            _emit_run_summary(
                run_summary_json=run_summary_json,
                started_at=started_at,
                finished_at=datetime.now(UTC),
                profile=profile,
                per_rsid=per_rsid_results,
                show_timings=show_timings,
            )
            _emit_timings_if_enabled(show_timings=show_timings)
            return ExitCode.API.value
        except OutputError as e:
            print(f"output error: {e}", file=sys.stderr, flush=True)
            per_rsid_results.append(
                PerRsidResult(
                    rsid=canonical_rsid,
                    name=None,
                    succeeded=False,
                    formats=list(formats),
                    output_paths=[],
                    snapshot_path=None,
                    error=str(e),
                ),
            )
            _emit_run_summary(
                run_summary_json=run_summary_json,
                started_at=started_at,
                finished_at=datetime.now(UTC),
                profile=profile,
                per_rsid=per_rsid_results,
                show_timings=show_timings,
            )
            _emit_timings_if_enabled(show_timings=show_timings)
            return ExitCode.OUTPUT.value
        except AaAutoSdrError as e:
            print(f"error: {e}", file=sys.stderr, flush=True)
            per_rsid_results.append(
                PerRsidResult(
                    rsid=canonical_rsid,
                    name=None,
                    succeeded=False,
                    formats=list(formats),
                    output_paths=[],
                    snapshot_path=None,
                    error=str(e),
                ),
            )
            _emit_run_summary(
                run_summary_json=run_summary_json,
                started_at=started_at,
                finished_at=datetime.now(UTC),
                profile=profile,
                per_rsid=per_rsid_results,
                show_timings=show_timings,
            )
            _emit_timings_if_enabled(show_timings=show_timings)
            return ExitCode.GENERIC.value

        for path in result.outputs:
            print(f"wrote: {path}")
        if first_output is None and result.outputs:
            first_output = result.outputs[0]
        per_rsid_results.append(
            PerRsidResult(
                rsid=canonical_rsid,
                name=result.report_suite_name,
                succeeded=True,
                formats=list(formats),
                output_paths=[str(p) for p in result.outputs],
                snapshot_path=None,
                error=None,
                quality_verdict=result.quality_verdict,
            ),
        )

        if result.git_op is not None and not result.git_op.ok:
            print(
                f"error: git {result.git_op.error_kind or 'operation'} failed: "
                f"{result.git_op.error_message or '(no message)'}",
                file=sys.stderr,
            )
            _emit_run_summary(
                run_summary_json=run_summary_json,
                started_at=started_at,
                finished_at=datetime.now(UTC),
                profile=profile,
                per_rsid=per_rsid_results,
                show_timings=show_timings,
            )
            _emit_timings_if_enabled(show_timings=show_timings)
            return int(ExitCode.SNAPSHOT)

    if auto_prune and snapshot_dir is not None:
        rc = _apply_auto_prune(
            is_pipe=is_pipe,
            snapshot_dir=snapshot_dir,
            rsids=canonical_rsids,
            keep_last=keep_last,
            keep_since=keep_since,
        )
        if rc != ExitCode.OK.value:
            _emit_run_summary(
                run_summary_json=run_summary_json,
                started_at=started_at,
                finished_at=datetime.now(UTC),
                profile=profile,
                per_rsid=per_rsid_results,
                show_timings=show_timings,
            )
            _emit_timings_if_enabled(show_timings=show_timings)
            return rc

    if open_after and not is_pipe and not dry_run and first_output is not None:
        from aa_auto_sdr.core._open import os_open

        os_open(first_output)

    _emit_run_summary(
        run_summary_json=run_summary_json,
        started_at=started_at,
        finished_at=datetime.now(UTC),
        profile=profile,
        per_rsid=per_rsid_results,
        show_timings=show_timings,
    )
    _emit_timings_if_enabled(show_timings=show_timings)

    # v1.12.0 — quality gate (single-RSID, file-output path). The build itself
    # succeeded; if any successful RSID's verdict is "fail", return
    # ExitCode.QUALITY (17). Per spec §3.10 this is a soft signal — outputs
    # already wrote successfully.
    if _fail_on_quality is not None and any(r.succeeded and r.quality_verdict == "fail" for r in per_rsid_results):
        return ExitCode.QUALITY.value
    return ExitCode.OK.value


def _apply_auto_prune(
    *,
    is_pipe: bool,
    snapshot_dir: Path,
    rsids: list[str],
    keep_last: int | None,
    keep_since: str | None,
) -> int:
    """Parse retention policy and prune each RSID under `snapshot_dir`.

    Returns ExitCode.OK on success, ExitCode.CONFIG on bad/empty policy.
    """
    from aa_auto_sdr.snapshot.retention import parse_policy
    from aa_auto_sdr.snapshot.store import prune_snapshots

    try:
        policy = parse_policy(keep_last=keep_last, keep_since=keep_since)
    except ConfigError as exc:
        msg = f"error: {exc}"
        _emit_pipe_or_print(is_pipe=is_pipe, exc=exc, message=msg, exit_code=ExitCode.CONFIG.value)
        return ExitCode.CONFIG.value
    if not policy.is_active():
        msg = "error: --auto-prune requires --keep-last or --keep-since"
        _emit_pipe_or_print(is_pipe=is_pipe, exc=None, message=msg, exit_code=ExitCode.CONFIG.value)
        return ExitCode.CONFIG.value
    for rs in rsids:
        prune_snapshots(snapshot_dir, policy, rsid=rs)
    return ExitCode.OK.value
