"""CLI entry point — argparse-based command interface for Orxaq.

Provides subcommands for credit risk, causal discovery, data fabric,
audit, and dashboard operations. All business logic delegated to OrxaqEngine.
Supports --json flag for structured output (ADR-023).
Stdlib-only (ADR-020).
"""

from __future__ import annotations

import argparse
import json
import sys

from orxaq.orchestration.integration import OrxaqEngine


def _load_data(args, engine: OrxaqEngine) -> list[dict]:
    """Load data from --data file or generate synthetic."""
    data_path = getattr(args, "data", None)
    if data_path:
        if data_path.endswith(".json"):
            from orxaq.fabric.connectors import JSONConnector

            conn = JSONConnector()
            ds = conn.read(data_path)
            return list(ds.data)
        from orxaq.fabric.connectors import CSVConnector

        csv_conn = CSVConnector()
        ds = csv_conn.read(data_path)
        return list(ds.data)

    engine.load_credit_ontology()
    return engine.generate_synthetic(n=1000, seed=42)


def _ensure_ontology(engine: OrxaqEngine) -> None:
    """Ensure credit ontology is loaded."""
    if engine.world_dag.node_count() == 0:
        engine.load_credit_ontology()


def _is_json(args) -> bool:
    """Check if --json flag is set."""
    return getattr(args, "json_output", False)


def _json_out(data: dict) -> int:
    """Print JSON and return 0."""
    print(json.dumps(data, indent=2, default=str))
    return 0


# ── Subcommand handlers ─────────────────────────────────────


def _cmd_status(args, engine: OrxaqEngine) -> int:
    st = engine.status()
    if _is_json(args):
        return _json_out(st)
    print("Orxaq Status")
    print(f"  DAG nodes:     {st['dag']['nodes']}")
    print(f"  DAG edges:     {st['dag']['edges']}")
    print(f"  Audit entries: {st['audit_entries']}")
    print(f"  Datasets:      {st['catalog']['total_datasets']}")
    return 0


def _cmd_credit_ontology(args, engine: OrxaqEngine) -> int:
    dag = engine.load_credit_ontology()
    if _is_json(args):
        entities = sorted(dag._entities.keys())
        edges = []
        for edge in dag._edges.values():
            edges.append(
                {
                    "source": edge.source,
                    "target": edge.target,
                    "mechanism": edge.mechanism,
                }
            )
        return _json_out(
            {
                "entities": entities,
                "edges": edges,
                "entity_count": len(entities),
            }
        )

    if getattr(args, "export", None):
        entities = sorted(dag._entities.keys())
        with open(args.export, "w") as f:
            json.dump({"variables": entities, "count": len(entities)}, f, indent=2)
        print(f"Exported {len(entities)} variables to {args.export}")
    else:
        from orxaq.credit.ontology import CREDIT_VARIABLES

        print(f"Credit Ontology: {len(CREDIT_VARIABLES)} variables\n")
        for name, desc in sorted(CREDIT_VARIABLES.items()):
            print(f"  {name:<25} {desc}")
    return 0


def _cmd_credit_pd(args, engine: OrxaqEngine) -> int:
    _ensure_ontology(engine)
    data = _load_data(args, engine)
    model = engine.fit_pd_model(data)

    if getattr(args, "borrower", None):
        borrower = json.loads(args.borrower)
        pd = model.predict_pd(borrower)
        if _is_json(args):
            return _json_out({"pd": pd})
        print(f"Predicted PD: {pd:.6f}")
    else:
        total_pd = sum(model.predict_pd(row) for row in data[:100])
        avg_pd = total_pd / min(len(data), 100)
        if _is_json(args):
            return _json_out({"avg_pd": avg_pd, "sample_size": min(len(data), 100)})
        print(f"Portfolio avg PD (sample 100): {avg_pd:.6f}")

    if getattr(args, "scenario", None):
        result = engine.stress_test(data, args.scenario)
        if _is_json(args):
            return _json_out(result)
        print(f"\nStress test ({result['scenario']}):")
        print(f"  Base PD:   {result['base_pd']:.6f}")
        print(f"  Stress PD: {result['stress_pd']:.6f}")
        print(f"  Delta:     {result['delta']:+.6f}")
    return 0


def _cmd_credit_ecl(args, engine: OrxaqEngine) -> int:
    _ensure_ontology(engine)
    data = _load_data(args, engine)

    horizon = None
    h_arg = getattr(args, "horizon", None)
    if h_arg and h_arg != "dynamic":
        horizon = int(h_arg)

    result = engine.compute_ecl(data, horizon=horizon)
    if _is_json(args):
        return _json_out(
            {
                "total_ecl": result.total_ecl,
                "pd_mean": result.pd_mean,
                "lgd": result.lgd,
                "ead_total": result.ead_total,
                "horizon_quarters": result.horizon_quarters,
                "horizon_method": result.horizon_method,
                "confidence_band": list(result.confidence_band),
                "ecl_by_segment": result.ecl_by_segment,
            }
        )
    print("CECL Expected Credit Loss")
    print(f"  Total ECL:    {result.total_ecl:.6f}")
    print(f"  Mean PD:      {result.pd_mean:.6f}")
    print(f"  LGD:          {result.lgd:.2f}")
    print(f"  EAD Total:    {result.ead_total:.2f}")
    print(f"  Horizon:      {result.horizon_quarters}Q ({result.horizon_method})")
    lo, hi = result.confidence_band
    print(f"  Confidence:   [{lo:.6f}, {hi:.6f}]")
    return 0


def _cmd_credit_fairness(args, engine: OrxaqEngine) -> int:
    _ensure_ontology(engine)
    protected = getattr(args, "protected", "age")
    decomp = engine.fairness_audit(protected=protected)
    if _is_json(args):
        return _json_out(
            {
                "protected_var": decomp.protected_var,
                "outcome_var": decomp.outcome_var,
                "total_effect": decomp.total_effect,
                "direct_effect": decomp.direct_effect,
                "indirect_effects": decomp.indirect_effects,
                "legitimate_paths": [list(p) for p in decomp.legitimate_paths],
                "concerning_paths": [list(p) for p in decomp.concerning_paths],
            }
        )
    print(f"Fair Lending Analysis: {decomp.protected_var} -> {decomp.outcome_var}")
    print(f"  Total effect:   {decomp.total_effect:.6f}")
    print(f"  Direct effect:  {decomp.direct_effect:.6f}")
    print(f"  Indirect paths: {len(decomp.indirect_effects)}")
    for mediator, effect in sorted(decomp.indirect_effects.items()):
        print(f"    via {mediator}: {effect:.6f}")
    print(f"  Legitimate paths:  {len(decomp.legitimate_paths)}")
    print(f"  Concerning paths:  {len(decomp.concerning_paths)}")
    return 0


def _cmd_credit_portfolio(args, engine: OrxaqEngine) -> int:
    from orxaq.credit.portfolio import LoanExposure, PortfolioAnalyzer, VintageAnalyzer

    _ensure_ontology(engine)
    data = _load_data(args, engine)

    # Convert raw data rows to LoanExposure objects
    exposures: list[LoanExposure] = []
    for row in data:
        exposures.append(
            LoanExposure(
                loan_id=str(row.get("loan_id", len(exposures))),
                pd=float(row.get("pd", row.get("default_flag", 0.05))),
                lgd=float(row.get("lgd", 0.45)),
                ead=float(row.get("ead", row.get("loan_amount", 0.0))),
                sector=str(row.get("sector", "unknown")),
                vintage=str(row.get("vintage", "unknown")),
            )
        )

    analyzer = PortfolioAnalyzer(exposures)
    m = analyzer.metrics()

    if _is_json(args):
        result: dict[str, object] = {
            "expected_loss": m.expected_loss,
            "unexpected_loss": m.unexpected_loss,
            "economic_capital": m.economic_capital,
            "hhi": m.hhi,
            "gini": m.gini,
            "count": m.count,
            "total_ead": m.total_ead,
        }
        if getattr(args, "vintage", False):
            va = VintageAnalyzer(exposures)
            vintages = va.by_vintage()
            result["vintages"] = {
                v: {
                    "expected_loss": vm.expected_loss,
                    "count": vm.count,
                    "total_ead": vm.total_ead,
                }
                for v, vm in vintages.items()
            }
        return _json_out(result)

    print("Portfolio Analytics")
    print(f"  Loans:             {m.count}")
    print(f"  Total EAD:         {m.total_ead:,.2f}")
    print(f"  Expected Loss:     {m.expected_loss:,.6f}")
    print(f"  Unexpected Loss:   {m.unexpected_loss:,.6f}")
    print(f"  Economic Capital:  {m.economic_capital:,.6f}")
    print(f"  HHI:               {m.hhi:.6f}")
    print(f"  Gini:              {m.gini:.6f}")

    if getattr(args, "vintage", False):
        va = VintageAnalyzer(exposures)
        vintages = va.by_vintage()
        print(f"\n  Vintage Analysis ({len(vintages)} cohorts)")
        for v, vm in sorted(vintages.items()):
            print(f"    {v}: {vm.count} loans, EL={vm.expected_loss:,.6f}")
    return 0


def _cmd_credit_stress(args, engine: OrxaqEngine) -> int:
    from orxaq.credit.portfolio import LoanExposure, PortfolioAnalyzer
    from orxaq.credit.scenarios import ADVERSE, BASELINE, SEVERELY_ADVERSE

    _ensure_ontology(engine)
    data = _load_data(args, engine)

    scenarios = {"baseline": BASELINE, "adverse": ADVERSE, "severely_adverse": SEVERELY_ADVERSE}
    scenario_name = getattr(args, "scenario", "adverse") or "adverse"
    scenario = scenarios.get(scenario_name)
    if scenario is None:
        print(f"Unknown scenario: {scenario_name}")
        return 1

    # Fit PD model and run stress test
    model = engine.fit_pd_model(data)

    # Get base and stressed PDs
    base_pds = [model.predict_pd(row) for row in data]
    stressed_pds = []
    for row in data:
        pds = [model.interventional_pd(row, var, val) for var, val in scenario.items()]
        stressed_pds.append(sum(pds) / len(pds) if pds else model.predict_pd(row))

    # Build base and stressed portfolios
    def _make_exposures(pd_list: list[float]) -> list[LoanExposure]:
        return [
            LoanExposure(
                loan_id=str(i),
                pd=pd_val,
                lgd=float(data[i].get("lgd", 0.45)),
                ead=float(data[i].get("ead", data[i].get("loan_amount", 0.0))),
                sector=str(data[i].get("sector", "unknown")),
                vintage=str(data[i].get("vintage", "unknown")),
            )
            for i, pd_val in enumerate(pd_list)
        ]

    base_m = PortfolioAnalyzer(_make_exposures(base_pds)).metrics()
    stress_m = PortfolioAnalyzer(_make_exposures(stressed_pds)).metrics()

    if _is_json(args):
        return _json_out(
            {
                "scenario": scenario_name,
                "base": {
                    "expected_loss": base_m.expected_loss,
                    "economic_capital": base_m.economic_capital,
                },
                "stressed": {
                    "expected_loss": stress_m.expected_loss,
                    "economic_capital": stress_m.economic_capital,
                },
                "delta_el": stress_m.expected_loss - base_m.expected_loss,
                "delta_ec": stress_m.economic_capital - base_m.economic_capital,
            }
        )

    print(f"Stress Test: {scenario_name}")
    print(f"  Loans: {base_m.count}")
    print(f"\n  {'Metric':<20} {'Base':>14} {'Stressed':>14} {'Delta':>14}")
    print(f"  {'-' * 62}")
    print(
        f"  {'Expected Loss':<20} {base_m.expected_loss:>14,.6f} "
        f"{stress_m.expected_loss:>14,.6f} "
        f"{stress_m.expected_loss - base_m.expected_loss:>+14,.6f}"
    )
    print(
        f"  {'Economic Capital':<20} {base_m.economic_capital:>14,.6f} "
        f"{stress_m.economic_capital:>14,.6f} "
        f"{stress_m.economic_capital - base_m.economic_capital:>+14,.6f}"
    )
    return 0


def _cmd_credit_report(args, engine: OrxaqEngine) -> int:
    from orxaq.credit.portfolio import LoanExposure, PortfolioAnalyzer
    from orxaq.credit.reporting import RiskReportBuilder
    from orxaq.credit.scenarios import ADVERSE, BASELINE, SEVERELY_ADVERSE

    _ensure_ontology(engine)
    data = _load_data(args, engine)

    # Fit model
    model = engine.fit_pd_model(data)

    # Build loan exposures
    exposures = [
        LoanExposure(
            loan_id=str(row.get("loan_id", i)),
            pd=model.predict_pd(row),
            lgd=float(row.get("lgd", 0.45)),
            ead=float(row.get("ead", row.get("loan_amount", 0.0))),
            sector=str(row.get("sector", "unknown")),
            vintage=str(row.get("vintage", "unknown")),
        )
        for i, row in enumerate(data)
    ]
    pm = PortfolioAnalyzer(exposures).metrics()

    builder = RiskReportBuilder()
    builder.add_portfolio_section(
        expected_loss=pm.expected_loss,
        unexpected_loss=pm.unexpected_loss,
        economic_capital=pm.economic_capital,
        hhi=pm.hhi,
        gini=pm.gini,
        count=pm.count,
        total_ead=pm.total_ead,
    )

    # ECL
    ecl = engine.compute_ecl(data)
    builder.add_ecl_section(
        total_ecl=ecl.total_ecl,
        horizon_quarters=ecl.horizon_quarters,
        horizon_method=ecl.horizon_method,
        pd_mean=ecl.pd_mean,
        lgd=ecl.lgd,
        confidence_low=ecl.confidence_band[0],
        confidence_high=ecl.confidence_band[1],
    )

    # Stress test
    scenario_name = getattr(args, "scenario", "adverse") or "adverse"
    scenarios = {"baseline": BASELINE, "adverse": ADVERSE, "severely_adverse": SEVERELY_ADVERSE}
    scenario = scenarios.get(scenario_name, ADVERSE)

    stressed_pds = []
    for row in data:
        pds = [model.interventional_pd(row, var, val) for var, val in scenario.items()]
        stressed_pds.append(sum(pds) / len(pds) if pds else model.predict_pd(row))

    stressed_exposures = [
        LoanExposure(
            loan_id=exposures[i].loan_id,
            pd=stressed_pds[i],
            lgd=exposures[i].lgd,
            ead=exposures[i].ead,
            sector=exposures[i].sector,
            vintage=exposures[i].vintage,
        )
        for i in range(len(exposures))
    ]
    sm = PortfolioAnalyzer(stressed_exposures).metrics()
    builder.add_stress_section(
        scenario_name=scenario_name,
        base_el=pm.expected_loss,
        stressed_el=sm.expected_loss,
        base_ec=pm.economic_capital,
        stressed_ec=sm.economic_capital,
    )

    # Fairness
    decomp = engine.fairness_audit(protected=getattr(args, "protected", "age") or "age")
    builder.add_fairness_section(
        protected_var=decomp.protected_var,
        total_effect=decomp.total_effect,
        direct_effect=decomp.direct_effect,
        indirect_paths=len(decomp.indirect_effects),
        concerning_paths=len(decomp.concerning_paths),
    )

    report = builder.build()

    output_path = getattr(args, "output", None)
    if output_path:
        import json as json_mod

        with open(output_path, "w") as f:
            json_mod.dump(report.to_dict(), f, indent=2)

    if _is_json(args):
        return _json_out(report.to_dict())

    print(report.to_text())
    if output_path:
        print(f"\nReport saved to {output_path}")
    return 0


def _cmd_credit_export_regulatory(args, engine: OrxaqEngine) -> int:
    """Export CECL/CCAR regulatory submission artifact via RegulatoryExporter."""
    import json as _json
    import sys

    from orxaq.credit.regulatory_export import RegulatoryExporter

    institution_id = getattr(args, "institution_id", "ORXAQ-DEMO-001") or "ORXAQ-DEMO-001"
    reporting_period = getattr(args, "reporting_period", "2024-Q4") or "2024-Q4"
    framework = getattr(args, "framework", "cecl") or "cecl"
    fmt = getattr(args, "format", "json") or "json"
    output_path = getattr(args, "output", None)
    scenarios = getattr(args, "scenarios", None)

    exporter = RegulatoryExporter(
        institution_id=institution_id,
        reporting_period=reporting_period,
    )

    if framework == "cecl":
        result = exporter.export_cecl(format=fmt)
    else:
        result = exporter.export_ccar(format=fmt, scenarios=scenarios or None)

    if fmt == "json":
        assert isinstance(result, dict)
        out = _json.dumps(result, indent=2, default=str)
    else:
        assert isinstance(result, str)
        out = result

    if output_path:
        with open(output_path, "w") as f:
            f.write(out)
        print(f"Regulatory export saved to {output_path}", file=sys.stderr)
    else:
        print(out)
    return 0


def _cmd_credit_regulatory_export(args, engine: OrxaqEngine) -> int:
    """Generate FR Y-14A / CCAR regulatory export package."""
    import csv
    import io
    import json as _json
    from datetime import UTC, datetime

    from orxaq.credit.cecl import CECLEngine
    from orxaq.credit.portfolio import LoanExposure
    from orxaq.credit.stress_report import FED_SCENARIOS, run_stress_test

    _ensure_ontology(engine)
    data = _load_data(args, engine)

    institution_id = getattr(args, "institution_id", "ORG001") or "ORG001"
    as_of = getattr(args, "as_of", "2025-12-31") or "2025-12-31"
    period = getattr(args, "period", "2025Q4") or "2025Q4"
    fmt = getattr(args, "format", "json") or "json"
    output_path = getattr(args, "output", None)

    model = engine.fit_pd_model(data)
    cecl_engine = CECLEngine(model, engine.world_dag)

    exposures = [
        LoanExposure(
            loan_id=str(row.get("loan_id", i)),
            pd=model.predict_pd(row),
            lgd=float(row.get("lgd", 0.45)),
            ead=float(row.get("ead", row.get("loan_amount", 0.0))),
            sector=str(row.get("sector", "unknown")),
            vintage=str(row.get("vintage", "unknown")),
        )
        for i, row in enumerate(data)
    ]

    stress_results: dict[str, dict] = {}
    for scenario_name in ("baseline", "adverse", "severely_adverse"):
        report = run_stress_test(
            portfolio=exposures,
            scenarios=FED_SCENARIOS,
            pd_model=model,
            cecl_engine=cecl_engine,
            scenario_name=scenario_name,
        )
        stress_results[scenario_name] = {
            "ecl": sum(report.quarterly_ecl),
            "portfolio_loss_rate": report.portfolio_loss_rate,
            "capital_impact": report.capital_impact,
        }

    sector_groups: dict[str, dict] = {}
    for exp in exposures:
        grp = sector_groups.setdefault(exp.sector, {"ead": 0.0, "pd": [], "lgd": [], "ecl": 0.0})
        grp["ead"] += exp.ead
        grp["pd"].append(exp.pd)
        grp["lgd"].append(exp.lgd)
        grp["ecl"] += exp.pd * exp.lgd * exp.ead

    schedule_h1 = [
        {
            "segment": name,
            "ead": g["ead"],
            "pd": sum(g["pd"]) / len(g["pd"]) if g["pd"] else 0.0,
            "lgd": sum(g["lgd"]) / len(g["lgd"]) if g["lgd"] else 0.45,
            "ecl": g["ecl"],
        }
        for name, g in sector_groups.items()
    ]

    generated_at = datetime.now(UTC).isoformat()
    pkg = {
        "as_of_date": as_of,
        "institution_id": institution_id,
        "submission_period": period,
        "schedule_h1": schedule_h1,
        "ccar_projection": stress_results,
        "scenario_comparison": {k: v["ecl"] for k, v in stress_results.items()},
        "generated_at": generated_at,
    }

    if fmt == "csv":
        buf = io.StringIO()
        if schedule_h1:
            writer = csv.DictWriter(buf, fieldnames=schedule_h1[0].keys())
            writer.writeheader()
            writer.writerows(schedule_h1)
        content = buf.getvalue()
    else:
        content = _json.dumps(pkg, indent=2, default=str)

    if output_path:
        with open(output_path, "w") as fh:
            fh.write(content)
        print(f"Regulatory package written to {output_path}")
    else:
        print(content)

    return 0


def _cmd_credit_regulatory_report(args, engine: OrxaqEngine) -> int:
    """Generate CECL/CCAR/DFAST regulatory HTML or Markdown report."""
    from orxaq.credit.cecl import CECLEngine
    from orxaq.credit.portfolio import LoanExposure
    from orxaq.credit.regulatory_report import RegulatorReportConfig, RegulatorReportExporter
    from orxaq.credit.reporting import RiskReportBuilder
    from orxaq.credit.stress_report import FED_SCENARIOS, run_stress_test

    _ensure_ontology(engine)
    data = _load_data(args, engine)

    institution = getattr(args, "institution", "Orxaq Financial, N.A.") or "Orxaq Financial, N.A."
    fmt = getattr(args, "format", "html") or "html"
    output_path = getattr(args, "output", None)
    scenario_name = getattr(args, "scenario", "severely_adverse") or "severely_adverse"

    model = engine.fit_pd_model(data)
    cecl_engine = CECLEngine(model, engine.world_dag)
    ecl = engine.compute_ecl(data)

    exposures = [
        LoanExposure(
            loan_id=str(row.get("loan_id", i)),
            pd=model.predict_pd(row),
            lgd=float(row.get("lgd", 0.45)),
            ead=float(row.get("ead", row.get("loan_amount", 0.0))),
            sector=str(row.get("sector", "unknown")),
            vintage=str(row.get("vintage", "unknown")),
        )
        for i, row in enumerate(data)
    ]

    stress_report = run_stress_test(
        portfolio=exposures,
        scenarios=FED_SCENARIOS,
        pd_model=model,
        cecl_engine=cecl_engine,
        scenario_name=scenario_name,
    )

    builder = RiskReportBuilder()
    builder.add_ecl_section(
        total_ecl=ecl.total_ecl,
        horizon_quarters=ecl.horizon_quarters,
        horizon_method=ecl.horizon_method,
        pd_mean=ecl.pd_mean,
        lgd=ecl.lgd,
        confidence_low=ecl.confidence_band[0],
        confidence_high=ecl.confidence_band[1],
    )
    risk_report = builder.build()

    cfg = RegulatorReportConfig(institution_name=institution)
    exporter = RegulatorReportExporter(
        stress_report=stress_report,
        risk_report=risk_report,
        ecl_result=ecl,
        portfolio=exposures,
        config=cfg,
    )

    if fmt == "markdown":
        content = exporter.to_markdown()
    else:
        content = exporter.to_html()

    if output_path:
        exporter.save(output_path, format=fmt)
        print(f"Regulatory report saved to {output_path}")
    else:
        truncated = content[:2000]
        print(truncated)
        if len(content) > 2000:
            print(f"\n... [{len(content) - 2000} chars — use --output to save]")
    return 0


def _cmd_credit_compare(args, engine: OrxaqEngine) -> int:
    """Compare stress test results across scenarios with delta analytics."""
    import json as _json
    import pathlib as _pathlib

    from orxaq.credit.causal_pd import CausalPDModel
    from orxaq.credit.cecl import CECLEngine
    from orxaq.credit.portfolio import LoanExposure
    from orxaq.credit.scenario_comparison import ScenarioComparator
    from orxaq.credit.stress_report import FED_SCENARIOS, run_stress_test
    from orxaq.credit.synthetic import CreditDataGenerator

    scenario_arg = (
        getattr(args, "scenarios", "baseline,adverse,severely_adverse")
        or "baseline,adverse,severely_adverse"
    )
    scenario_names = [s.strip() for s in scenario_arg.split(",") if s.strip()]
    fmt = getattr(args, "format", "text") or "text"
    starting_cet1 = float(getattr(args, "starting_cet1", 0.12) or 0.12)
    output_path = getattr(args, "output", None)

    _ensure_ontology(engine)
    gen = CreditDataGenerator(engine.world_dag, seed=42)
    data = gen.generate(200)
    pd_model = CausalPDModel(engine.world_dag)
    pd_model.fit(data)
    cecl_engine = CECLEngine(pd_model, engine.world_dag)

    exposures = [
        LoanExposure(
            loan_id=str(i),
            pd=max(0.001, min(0.999, float(rec.get("default_flag", 0.05)))),
            lgd=0.45,
            ead=float(rec.get("loan_amount", 10_000.0)),
            sector=str(rec.get("sector", "retail")),
            vintage="2024",
        )
        for i, rec in enumerate(data[:100])
    ]

    reports = {}
    for name in scenario_names:
        if name not in FED_SCENARIOS:
            print(f"Unknown scenario: {name}. Choose from: {list(FED_SCENARIOS)}")
            return 1
        reports[name] = run_stress_test(
            exposures, FED_SCENARIOS, pd_model, cecl_engine, scenario_name=name
        )

    comparator = ScenarioComparator(reports)
    result = comparator.compare(starting_cet1=starting_cet1)

    if fmt == "json":
        output_str = _json.dumps(result.to_dict(), indent=2)
    else:
        lines = [
            f"Scenario Comparison — {len(scenario_names)} scenarios, "
            f"{len(result.quarter_labels)} quarters"
        ]
        lines.append("-" * 60)
        for scen in result.scenarios:
            wf = result.capital_waterfall.get(scen, {})
            peak = result.peak_loss_quarter.get(scen, "?")
            lines.append(f"\n{scen.upper()}")
            lines.append(f"  Peak loss quarter : {peak}")
            lines.append(f"  Starting CET1     : {wf.get('starting_cet1', 0):.2%}")
            lines.append(f"  Credit losses     : {wf.get('credit_losses', 0):.4%}")
            lines.append(f"  PPNR offset       : {wf.get('ppnr_offset', 0):.4%}")
            lines.append(f"  Ending CET1       : {wf.get('ending_cet1', 0):.2%}")
        lines.append("\nTop sensitivity drivers:")
        top3 = sorted(result.sensitivity_attribution.items(), key=lambda x: x[1], reverse=True)[:3]
        for var, share in top3:
            lines.append(f"  {var:<25} {share:.1%}")
        output_str = "\n".join(lines)

    if output_path:
        _pathlib.Path(output_path).write_text(output_str, encoding="utf-8")
        print(f"Scenario comparison written to {output_path}")
    else:
        print(output_str)
    return 0


def _cmd_discover(args, engine: OrxaqEngine) -> int:
    data = _load_data(args, engine)
    variables = None
    if getattr(args, "variables", None):
        variables = [v.strip() for v in args.variables.split(",")]

    alpha = getattr(args, "alpha", 0.05) or 0.05
    use_consensus = getattr(args, "consensus", False)

    result = engine.discover_structure(
        data, variables=variables, alpha=alpha, use_consensus=use_consensus
    )

    if hasattr(result, "edges") and hasattr(result, "algorithms_used"):
        if _is_json(args):
            return _json_out(
                {
                    "edges": [
                        {
                            "source": e.source,
                            "target": e.target,
                            "directed": e.directed,
                            "support_count": e.support_count,
                            "confidence": e.confidence,
                            "supporters": list(e.supporters),
                        }
                        for e in result.edges
                    ],
                    "algorithms_used": list(result.algorithms_used),
                    "agreement_threshold": result.agreement_threshold,
                }
            )
        print(f"Consensus Discovery ({len(result.edges)} edges)")
        print(f"  Algorithms: {', '.join(result.algorithms_used)}")
        print(f"  Threshold:  {result.agreement_threshold:.2f}\n")
        for edge in result.edges:
            arrow = "->" if edge.directed else "--"
            conf = f"[{edge.confidence}]"
            supporters = ",".join(edge.supporters)
            print(f"  {edge.source} {arrow} {edge.target}  {conf}  ({supporters})")
    else:
        if _is_json(args):
            return _json_out(
                {
                    "skeleton": [list(e) for e in sorted(result.skeleton)],
                    "oriented": [list(e) for e in sorted(result.oriented)],
                    "ci_tests_run": result.ci_tests_run,
                }
            )
        print(f"PC Discovery ({len(result.skeleton)} skeleton edges)")
        print(f"  CI tests run: {result.ci_tests_run}")
        print(f"  Oriented:     {len(result.oriented)}\n")
        for a, b in sorted(result.skeleton):
            if (a, b) in result.oriented:
                arrow = "->"
            elif (b, a) in result.oriented:
                arrow = "<-"
            else:
                arrow = "--"
            print(f"  {a} {arrow} {b}")
    return 0


def _cmd_validate(args, engine: OrxaqEngine) -> int:
    # Mode 1: Generate synthetic validation dataset.
    if getattr(args, "generate", False):
        return _cmd_validate_generate(args)

    # Mode 2: Full pipeline validation (--ground-truth provided).
    gt_path = getattr(args, "ground_truth", None)
    if gt_path:
        return _cmd_validate_pipeline(args)

    # Mode 3: Default d-separation validation.
    _ensure_ontology(engine)
    data = _load_data(args, engine)
    alpha = getattr(args, "alpha", 0.05) or 0.05
    report = engine.validate_dag(data, alpha=alpha)
    if _is_json(args):
        return _json_out(
            {
                "total_implications": report.total_implications,
                "violations": report.violations,
                "violation_rate": report.violation_rate,
                "passed": report.passed,
            }
        )
    status = "PASSED" if report.passed else "FAILED"
    print(f"D-Separation Validation: {status}")
    print(f"  Total implications: {report.total_implications}")
    print(f"  Violations:         {report.violations}")
    print(f"  Violation rate:     {report.violation_rate:.2%}")
    if report.worst_violations:
        print("\n  Worst violations:")
        for v in report.worst_violations[:5]:
            z_str = ",".join(sorted(v.z_set)) if v.z_set else "{}"
            print(f"    {v.x} _||_ {v.y} | {z_str}  p={v.p_value:.4f}")
    return 0


def _cmd_validate_generate(args) -> int:
    from orxaq.fabric.validation.synthetic import ValidationDataGenerator

    gen = ValidationDataGenerator()
    rows = gen.generate(n=args.rows, seed=args.seed)
    output = getattr(args, "output", None) or "data/validation/credit_portfolio.csv"
    gen.save_csv(rows, output)
    print(f"Generated {len(rows)} rows to {output}")
    return 0


def _cmd_validate_pipeline(args) -> int:
    from orxaq.fabric.validation.pipeline import ValidationPipeline
    from orxaq.fabric.validation.report import generate_report, save_report

    data_path = getattr(args, "data", None)
    gt_path = args.ground_truth
    algorithm = getattr(args, "algorithm", "pc")

    pipe = ValidationPipeline(data_path=data_path, ground_truth_path=gt_path, algorithm=algorithm)

    if data_path:
        pipe.load_data()
    else:
        from orxaq.fabric.validation.synthetic import ValidationDataGenerator

        gen = ValidationDataGenerator(gt_path)
        data = gen.generate(n=1000, seed=getattr(args, "seed", 42))
        pipe.load_data(data)

    pipe.load_ground_truth()
    variables = pipe._variables[:5]
    pipe.discover(variables)
    metrics = pipe.evaluate()

    report = generate_report(
        algorithm=algorithm,
        variables_count=len(variables),
        data_rows=len(pipe._data),
        metrics=metrics,
        predicted_edges=pipe.predicted_edges,
        true_edges=pipe.true_edges,
    )

    output_path = getattr(args, "output", None)
    if output_path:
        save_report(report, output_path)

    if _is_json(args):
        return _json_out(report.to_dict())

    print("Validation Pipeline Report")
    print(f"  Algorithm:    {algorithm}")
    print(f"  Variables:    {len(variables)}")
    print(f"  Data rows:    {len(pipe._data)}")
    print(f"  SHD:          {metrics.shd}")
    print(f"  F1 Score:     {metrics.f1:.3f}")
    print(f"  Precision:    {metrics.precision:.3f}")
    print(f"  Recall:       {metrics.recall:.3f}")
    print(f"  Orientation:  {metrics.orientation_accuracy:.3f}")
    print(f"  FDR:          {metrics.fdr:.3f}")
    if output_path:
        print(f"\n  Report saved to {output_path}")
    return 0


def _cmd_fabric_ingest(args, engine: OrxaqEngine) -> int:
    filepath = args.file
    schema_name = getattr(args, "schema", None)
    dataset = engine.ingest_csv(filepath, schema_name=schema_name)
    if _is_json(args):
        return _json_out(
            {
                "rows": dataset.row_count,
                "source": dataset.source,
                "connector": dataset.connector_type,
            }
        )
    print(f"Ingested {dataset.row_count} rows from {dataset.source}")
    print(f"  Connector: {dataset.connector_type}")
    print(f"  Columns:   {len(dataset.data[0]) if dataset.data else 0}")
    return 0


def _cmd_fabric_profile(args, engine: OrxaqEngine) -> int:
    data = _load_data(args, engine)
    profile = engine.profile_data(data)
    if _is_json(args):
        cols = []
        for col in profile.columns.values():
            c = {"name": col.name, "dtype": col.dtype}
            if col.dtype in ("float", "int"):
                c.update(
                    {
                        "mean": col.mean,
                        "std": col.std,
                        "min": col.min_val,
                        "max": col.max_val,
                    }
                )
            else:
                c.update({"unique_count": col.unique_count, "null_count": col.null_count})
            cols.append(c)
        return _json_out(
            {
                "row_count": profile.row_count,
                "column_count": profile.column_count,
                "columns": cols,
            }
        )
    print(f"Data Profile: {profile.row_count} rows, {profile.column_count} columns\n")
    for col in profile.columns.values():
        if col.dtype in ("float", "int"):
            print(f"  {col.name}:")
            print(f"    mean={col.mean:.4f}  std={col.std:.4f}")
            print(f"    min={col.min_val:.4f}   max={col.max_val:.4f}")
        else:
            print(f"  {col.name}:")
            print(f"    type={col.dtype}  unique={col.unique_count}  nulls={col.null_count}")
    return 0


def _cmd_fabric_quality(args, engine: OrxaqEngine) -> int:
    data = _load_data(args, engine)
    gate_names = None
    if getattr(args, "gates", None):
        gate_names = [g.strip() for g in args.gates.split(",")]
    result = engine.run_quality_gates(data, gate_names=gate_names)
    if _is_json(args):
        return _json_out(
            {
                "all_passed": result.all_passed,
                "overall_score": result.overall_score,
                "gates": [
                    {
                        "name": gr.gate_name,
                        "passed": gr.passed,
                        "score": gr.score,
                        "failures": len(gr.failures),
                    }
                    for gr in result.gate_results
                ],
            }
        )
    status = "PASSED" if result.all_passed else "FAILED"
    print(f"Quality Gates: {status} (score={result.overall_score:.2f})")
    for gr in result.gate_results:
        s = "PASS" if gr.passed else "FAIL"
        print(f"  {gr.gate_name}: {s} (score={gr.score:.2f}, failures={len(gr.failures)})")
    return 0


def _cmd_audit(args, engine: OrxaqEngine) -> int:
    entries = engine.audit_log.entries()
    last_n = getattr(args, "last", 20) or 20
    action_filter = getattr(args, "action", None)
    actor_filter = getattr(args, "actor", None)

    if action_filter:
        entries = [e for e in entries if action_filter in e.action]
    if actor_filter:
        entries = [e for e in entries if actor_filter in e.actor]

    entries = entries[-last_n:]
    if _is_json(args):
        return _json_out(
            {
                "entries": [
                    {
                        "timestamp": e.timestamp,
                        "action": e.action,
                        "actor": e.actor,
                        "details": e.details,
                    }
                    for e in entries
                ]
            }
        )
    print(f"Audit Log ({len(entries)} entries)")
    for entry in entries:
        print(f"  [{entry.timestamp}] {entry.action} by {entry.actor}: {entry.details}")
    if not entries:
        print("  (empty)")
    return 0


def _cmd_serve(args, engine: OrxaqEngine) -> int:
    from orxaq.experiences.server import serve

    port = getattr(args, "port", 8741) or 8741
    skin = getattr(args, "skin", "gamma") or "gamma"
    serve(port=port, skin=skin)
    return 0


def _cmd_edit(args, engine: OrxaqEngine) -> int:
    from orxaq.experiences.server import serve

    port = getattr(args, "port", 8742) or 8742
    skin = getattr(args, "skin", "gamma") or "gamma"
    print("Opening DAG Editor...")
    import webbrowser

    webbrowser.open(f"http://localhost:{port}/editor")
    serve(port=port, skin=skin, open_browser=False)
    return 0


def _cmd_simulate(args, engine: OrxaqEngine) -> int:
    from orxaq.intelligence.simulator import (
        BUILT_IN_SCENARIOS,
        Scenario,
        ScenarioSimulator,
    )

    _ensure_ontology(engine)
    scenario_name = getattr(args, "scenario", "adverse") or "adverse"

    scenario_file = getattr(args, "file", None)
    if scenario_file:
        scenario = Scenario.from_json(scenario_file)
    elif scenario_name in BUILT_IN_SCENARIOS:
        scenario = BUILT_IN_SCENARIOS[scenario_name]
    else:
        print(f"Unknown scenario: {scenario_name}")
        return 1

    sim = ScenarioSimulator(engine.world_dag, seed=getattr(args, "seed", 42))

    mc_runs = getattr(args, "monte_carlo", 0)
    if mc_runs and mc_runs > 0:
        mc = sim.monte_carlo(scenario, n_simulations=mc_runs)
        if _is_json(args):
            return _json_out(mc)
        print(f"Monte Carlo: {scenario.name} ({mc_runs} runs)")
        print(
            f"  Impact score: {mc['impact_score_mean']:.4f} "
            f"[{mc['impact_score_p5']:.4f}, {mc['impact_score_p95']:.4f}]"
        )
        for var_id, stats in sorted(mc["variable_stats"].items()):
            print(
                f"  {var_id}: mean={stats['mean_delta']:.6f} "
                f"[{stats['p5']:.6f}, {stats['p95']:.6f}]"
            )
        return 0

    result = sim.simulate(scenario)
    if _is_json(args):
        return _json_out(result.to_dict())

    print(f"Scenario: {scenario.name} ({scenario.severity})")
    print(f"  Description: {scenario.description}")
    print(f"  Affected variables: {result.total_affected}")
    print(f"  Max propagation depth: {result.max_depth}")
    print(f"  Impact score: {result.impact_score:.4f}")
    print()
    for step in result.steps:
        src = f" (from {', '.join(step.source_variables)})" if step.source_variables else " (shock)"
        print(
            f"  [{step.depth}] {step.variable}: "
            f"{step.original_value:.4f} -> {step.new_value:.4f} "
            f"(delta={step.delta:+.4f}){src}"
        )
    return 0


def _cmd_providers_check(args, _engine: OrxaqEngine) -> int:
    from orxaq.orchestration.providers.registry import ProviderRegistry

    registry = ProviderRegistry.default()
    results = registry.health_check_all()
    if _is_json(args):
        return _json_out(results)
    print("Provider Health Check")
    for name, info in sorted(results.items()):
        status = "OK" if info.get("healthy") else "FAIL"
        error = info.get("error", "")
        models = ", ".join(info.get("models", []))
        print(f"  {name:<12} [{status}]  models: {models}")
        if error:
            print(f"               error: {error}")
    return 0


def _cmd_providers_cost(args, _engine: OrxaqEngine) -> int:
    from orxaq.orchestration.providers.cost import DEFAULT_PRICING, CostTracker

    tracker = CostTracker()
    if _is_json(args):
        pricing = {}
        for provider, models in DEFAULT_PRICING.items():
            pricing[provider] = {
                m: {"input_per_1k": v[0], "output_per_1k": v[1]} for m, v in models.items()
            }
        return _json_out({"pricing": pricing, "usage": tracker.summary()})
    print("Provider Pricing (per 1K tokens)")
    for provider, models in sorted(DEFAULT_PRICING.items()):
        print(f"  {provider}:")
        for model, (inp, out) in sorted(models.items()):
            print(f"    {model:<20} input=${inp:.5f}  output=${out:.5f}")
    return 0


def _cmd_cost_status(args, _engine: OrxaqEngine) -> int:
    from orxaq.orchestration.providers.cost import BudgetManager

    mgr = BudgetManager()
    st = mgr.status()
    if _is_json(args):
        return _json_out(st)
    print("Budget Status")
    print(f"  Monthly budget: ${st['monthly_budget_usd']:.2f}")
    print(f"  Spent:          ${st['spent_usd']:.4f}")
    print(f"  Remaining:      ${st['remaining_usd']:.2f}")
    print(f"  Daily budget:   ${st['daily_budget_usd']:.2f}")
    print(f"  Total calls:    {st['total_calls']}")
    status_str = "OK" if st["within_budget"] else "OVER BUDGET"
    if st["warning"]:
        status_str = "WARNING (>80%)"
    print(f"  Status:         {status_str}")
    return 0


def _cmd_cost_set(args, _engine: OrxaqEngine) -> int:
    from orxaq.orchestration.providers.cost import BudgetManager

    budget = getattr(args, "budget_amount", 500.0)
    mgr = BudgetManager()
    mgr.set_budget(budget)
    if _is_json(args):
        return _json_out({"monthly_budget_usd": budget, "updated": True})
    print(f"Budget set to ${budget:.2f}/month")
    print(f"  Daily:     ${budget / 30:.2f}")
    print(f"  Soft limit: ${budget * 0.8:.2f} (80%)")
    print(f"  Hard limit: ${budget:.2f}")
    return 0


def _cmd_providers_list(args, _engine: OrxaqEngine) -> int:
    from orxaq.orchestration.providers.registry import ProviderRegistry

    registry = ProviderRegistry.default()
    models = registry.available_models()
    if _is_json(args):
        return _json_out(models)
    print("Available Models by Provider")
    for name, model_list in sorted(models.items()):
        print(f"  {name}:")
        for m in model_list:
            print(f"    - {m}")
    return 0


def _cmd_providers_cache(args, _engine: OrxaqEngine) -> int:
    from orxaq.kernel.cache import get_registry

    cache = get_registry().get("provider_router")
    if cache is None:
        if _is_json(args):
            return _json_out({"enabled": False, "entries": 0, "hits": 0, "misses": 0})
        print("Provider Cache: not active (no router instantiated)")
        return 0
    total = cache.hits + cache.misses
    hit_rate = cache.hits / total if total else 0.0
    if _is_json(args):
        return _json_out(
            {
                "enabled": True,
                "entries": len(cache),
                "hits": cache.hits,
                "misses": cache.misses,
                "evictions": cache.evictions,
                "hit_rate": round(hit_rate, 4),
            }
        )
    print("Provider Response Cache")
    print(f"  Entries:    {len(cache)}")
    print(f"  Hits:       {cache.hits}")
    print(f"  Misses:     {cache.misses}")
    print(f"  Evictions:  {cache.evictions}")
    print(f"  Hit rate:   {hit_rate:.1%}")
    return 0


def _cmd_providers_stats(args, _engine: OrxaqEngine) -> int:
    from orxaq.orchestration.providers.registry import ProviderRegistry
    from orxaq.orchestration.providers.router import ProviderRouter

    registry = ProviderRegistry.default()
    router = ProviderRouter(registry)
    stats = router.stats()
    if _is_json(args):
        return _json_out(stats)
    print("Provider Routing Statistics")
    print(f"  Priority: {' > '.join(stats['priority'])}")
    cache = stats["cache"]
    print(
        f"  Cache:    {'enabled' if cache['enabled'] else 'disabled'} "
        f"({cache['entries']} entries, {cache['hit_rate']:.0%} hit rate)"
    )
    if stats["latency"]:
        print("  Latency:")
        for name, lat in sorted(stats["latency"].items()):
            print(f"    {name:<12} {lat['calls']} calls, avg {lat['avg_ms']:.1f}ms")
    if stats["failures"]:
        print("  Failures:")
        for name, count in sorted(stats["failures"].items()):
            if count > 0:
                print(f"    {name:<12} {count}")
    return 0


# ── Model subcommand handlers ────────────────────────────────


def _cmd_model_save(args, engine: OrxaqEngine) -> int:
    from orxaq.kernel.checkpoint import CheckpointStore, create_checkpoint

    store = CheckpointStore(args.store)
    cp = create_checkpoint(engine.world_dag, name=args.name, version=args.version)
    path = store.save(cp)
    if _is_json(args):
        return _json_out(
            {
                "saved": True,
                "name": cp.name,
                "version": cp.version,
                "content_hash": cp.content_hash,
                "entities": len(cp.entities),
                "edges": len(cp.edges),
                "path": str(path),
            }
        )
    print(f"Saved checkpoint: {cp.name}@{cp.version}")
    print(f"  Entities: {len(cp.entities)}")
    print(f"  Edges:    {len(cp.edges)}")
    print(f"  Hash:     {cp.content_hash[:16]}...")
    print(f"  Path:     {path}")
    return 0


def _cmd_model_load(args, engine: OrxaqEngine) -> int:
    from orxaq.kernel.checkpoint import Checkpoint, CheckpointStore, restore_from_checkpoint

    store = CheckpointStore(args.store)
    version = args.version
    cp: Checkpoint | None
    if version:
        cp = store.load(args.name, version)
    else:
        cp = store.latest(args.name)
        if cp is None:
            if _is_json(args):
                return _json_out({"error": f"No checkpoints for model '{args.name}'"})
            print(f"Error: No checkpoints for model '{args.name}'")
            return 1

    dag = restore_from_checkpoint(cp)
    # Replace engine's DAG with restored version.
    engine.world_dag = dag
    if _is_json(args):
        return _json_out(
            {
                "loaded": True,
                "name": cp.name,
                "version": cp.version,
                "entities": len(cp.entities),
                "edges": len(cp.edges),
                "content_hash": cp.content_hash,
            }
        )
    print(f"Loaded checkpoint: {cp.name}@{cp.version}")
    print(f"  Entities: {len(cp.entities)}")
    print(f"  Edges:    {len(cp.edges)}")
    print(f"  Hash:     {cp.content_hash[:16]}...")
    return 0


def _cmd_model_list(args, _engine: OrxaqEngine) -> int:
    from orxaq.kernel.checkpoint import CheckpointStore

    store = CheckpointStore(args.store)
    models = store.list_models()
    if _is_json(args):
        result = []
        for name in models:
            versions = store.list_versions(name)
            latest = versions[0] if versions else None
            result.append({"name": name, "versions": versions, "latest": latest})
        return _json_out({"models": result})
    if not models:
        print("No saved models")
        return 0
    print(f"Saved Models ({len(models)})")
    for name in models:
        versions = store.list_versions(name)
        latest = versions[0] if versions else "none"
        print(f"  {name:<20} {len(versions)} version(s), latest: {latest}")
    return 0


def _cmd_model_versions(args, _engine: OrxaqEngine) -> int:
    from orxaq.kernel.checkpoint import CheckpointStore

    store = CheckpointStore(args.store)
    versions = store.list_versions(args.name)
    if _is_json(args):
        details = []
        for v in versions:
            cp = store.load(args.name, v)
            details.append(
                {
                    "version": v,
                    "entities": len(cp.entities),
                    "edges": len(cp.edges),
                    "content_hash": cp.content_hash,
                    "timestamp": cp.timestamp,
                }
            )
        return _json_out({"name": args.name, "versions": details})
    if not versions:
        print(f"No versions for model '{args.name}'")
        return 0
    print(f"Versions of '{args.name}' ({len(versions)})")
    for v in versions:
        cp = store.load(args.name, v)
        h = cp.content_hash[:12]
        print(f"  {v:<12} {len(cp.entities)} entities, {len(cp.edges)} edges, hash={h}...")
    return 0


def _cmd_model_diff(args, _engine: OrxaqEngine) -> int:
    from orxaq.kernel.checkpoint import CheckpointStore

    store = CheckpointStore(args.store)
    cp1 = store.load(args.name, args.v1)
    cp2 = store.load(args.name, args.v2)

    e1_ids = {e["entity_id"] for e in cp1.entities}
    e2_ids = {e["entity_id"] for e in cp2.entities}
    added_entities = sorted(e2_ids - e1_ids)
    removed_entities = sorted(e1_ids - e2_ids)

    edge1_keys = {(e["source"], e["target"]) for e in cp1.edges}
    edge2_keys = {(e["source"], e["target"]) for e in cp2.edges}
    added_edges = sorted(edge2_keys - edge1_keys)
    removed_edges = sorted(edge1_keys - edge2_keys)

    same_hash = cp1.content_hash == cp2.content_hash

    if _is_json(args):
        return _json_out(
            {
                "name": args.name,
                "v1": args.v1,
                "v2": args.v2,
                "identical": same_hash,
                "entities": {
                    "v1_count": len(e1_ids),
                    "v2_count": len(e2_ids),
                    "added": added_entities,
                    "removed": removed_entities,
                },
                "edges": {
                    "v1_count": len(edge1_keys),
                    "v2_count": len(edge2_keys),
                    "added": [list(e) for e in added_edges],
                    "removed": [list(e) for e in removed_edges],
                },
            }
        )

    print(f"Diff: {args.name}@{args.v1} vs {args.name}@{args.v2}")
    if same_hash:
        print("  Identical (same content hash)")
        return 0
    print(f"  Entities: {len(e1_ids)} -> {len(e2_ids)}")
    if added_entities:
        for e in added_entities:
            print(f"    + {e}")
    if removed_entities:
        for e in removed_entities:
            print(f"    - {e}")
    print(f"  Edges: {len(edge1_keys)} -> {len(edge2_keys)}")
    if added_edges:
        for src, tgt in added_edges:
            print(f"    + {src} -> {tgt}")
    if removed_edges:
        for src, tgt in removed_edges:
            print(f"    - {src} -> {tgt}")
    return 0


# ── Export subcommand handlers ────────────────────────────────


def _cmd_model_regression(args, _engine: OrxaqEngine) -> int:
    """Run regression test between two model checkpoints."""
    from orxaq.kernel.checkpoint import CheckpointStore
    from orxaq.kernel.regression import run_regression_test

    store_dir = getattr(args, "store", ".orxaq/checkpoints") or ".orxaq/checkpoints"
    name = str(getattr(args, "name", None) or "")
    v1 = str(getattr(args, "v1", None) or "")
    v2 = str(getattr(args, "v2", None) or "")
    store = CheckpointStore(store_dir)
    try:
        baseline = store.load(name, v1)
        candidate = store.load(name, v2)
    except FileNotFoundError as exc:
        print(f"Checkpoint not found: {exc}")
        return 1

    report = run_regression_test(baseline, candidate)
    if _is_json(args):
        return _json_out(
            {
                "passed": report.passed,
                "structural": report.structural.to_dict(),
                "metrics": [m.to_dict() for m in report.metrics],
                "regression_count": report.regression_count,
                "notes": report.notes,
            }
        )
    status = "PASS" if report.passed else "FAIL"
    print(f"Regression: {report.baseline_name} -> {report.candidate_name}  [{status}]")
    print(f"Baseline: {report.baseline_name}  Candidate: {report.candidate_name}")
    if report.structural.has_changes:
        print(f"  Structural changes: {report.structural.change_count}")
    for note in report.notes:
        print(f"  {note}")
    return 0


def _cmd_model_metrics(args, _engine: OrxaqEngine) -> int:
    """Show DAG metrics for a model checkpoint."""
    from orxaq.kernel.checkpoint import CheckpointStore, restore_from_checkpoint
    from orxaq.kernel.regression import dag_metrics

    store_dir = getattr(args, "store", ".orxaq/checkpoints") or ".orxaq/checkpoints"
    name = str(getattr(args, "name", None) or "")
    version = getattr(args, "version", None)
    store = CheckpointStore(store_dir)

    versions = store.list_versions(name)
    if not versions:
        print(f"Model not found: {name}")
        return 1

    target_version = str(version or versions[0])
    try:
        cp = store.load(name, target_version)
    except FileNotFoundError:
        print(f"Checkpoint not found: {name}@{target_version}")
        return 1

    dag = restore_from_checkpoint(cp)
    metrics = dag_metrics(dag)

    if _is_json(args):
        return _json_out(
            {
                "name": name,
                "version": target_version,
                "metrics": metrics,
            }
        )
    print(f"Model: {name}  Version: {target_version}")
    for k, v in sorted(metrics.items()):
        print(f"  {k}: {v}")
    return 0


def _cmd_export_dag(args, engine: OrxaqEngine) -> int:
    fmt = getattr(args, "format", "json")
    output = getattr(args, "output", None)

    if fmt == "csv":
        from orxaq.fabric.exporters import ResultExporter, export_dag_summary

        records = export_dag_summary(engine.world_dag)
        content = ResultExporter.to_csv(records)
    elif fmt == "dot":
        from orxaq.experiences.dag_editor.export import DAGExporter

        content = DAGExporter(engine.world_dag).to_dot()
    elif fmt == "mermaid":
        from orxaq.experiences.dag_editor.export import DAGExporter

        content = DAGExporter(engine.world_dag).to_mermaid()
    else:
        from orxaq.experiences.dag_editor.export import DAGExporter

        content = DAGExporter(engine.world_dag).to_json()

    if output:
        from orxaq.fabric.exporters import ResultExporter

        path = ResultExporter.write(content, output)
        if _is_json(args):
            return _json_out({"exported": True, "format": fmt, "path": str(path)})
        print(f"Exported DAG ({fmt}) to {path}")
        return 0

    if _is_json(args) and fmt == "json":
        import json

        return _json_out(json.loads(content))
    print(content)
    return 0


def _cmd_export_ecl(args, engine: OrxaqEngine) -> int:
    from orxaq.fabric.exporters import ResultExporter, export_ecl_results

    fmt = getattr(args, "format", "json")
    output = getattr(args, "output", None)
    _ensure_ontology(engine)
    data = _load_data(args, engine)
    result = engine.compute_ecl(data)
    records = export_ecl_results(result)

    content = ResultExporter.to_csv(records) if fmt == "csv" else ResultExporter.to_json(records)

    if output:
        path = ResultExporter.write(content, output)
        if _is_json(args):
            return _json_out(
                {"exported": True, "format": fmt, "records": len(records), "path": str(path)}
            )
        print(f"Exported ECL ({fmt}, {len(records)} segments) to {path}")
        return 0

    if _is_json(args) and fmt == "json":
        import json

        return _json_out({"records": json.loads(content)})
    print(content)
    return 0


def _cmd_export_edges(args, engine: OrxaqEngine) -> int:
    from orxaq.fabric.exporters import DAGCSVExporter, ResultExporter

    content = DAGCSVExporter.export_edges(engine.world_dag)
    output = getattr(args, "output", None)

    if output:
        path = ResultExporter.write(content, output)
        if _is_json(args):
            return _json_out({"exported": True, "path": str(path)})
        print(f"Exported edges to {path}")
        return 0

    print(content, end="")
    return 0


def _cmd_export_nodes(args, engine: OrxaqEngine) -> int:
    from orxaq.fabric.exporters import DAGCSVExporter, ResultExporter

    content = DAGCSVExporter.export_nodes(engine.world_dag)
    output = getattr(args, "output", None)

    if output:
        path = ResultExporter.write(content, output)
        if _is_json(args):
            return _json_out({"exported": True, "path": str(path)})
        print(f"Exported nodes to {path}")
        return 0

    print(content, end="")
    return 0


# ── Batch subcommand handlers ────────────────────────────────

_batch_queue = None


def _get_batch_queue():
    """Get or create the singleton BatchQueue."""
    global _batch_queue  # noqa: PLW0603
    if _batch_queue is None:
        from orxaq.orchestration.batch import BatchQueue

        _batch_queue = BatchQueue()
        _batch_queue.register("ecl", _batch_ecl_handler)
        _batch_queue.register("discover", _batch_discover_handler)
        _batch_queue.register("fairness", _batch_fairness_handler)
    return _batch_queue


def _batch_ecl_handler(**kwargs):
    """Run ECL computation as batch job."""
    engine = OrxaqEngine()
    engine.load_credit_ontology()
    data = engine.generate_synthetic(
        n=kwargs.get("n", 1000),
        seed=kwargs.get("seed", 42),
    )
    horizon = kwargs.get("horizon")
    result = engine.compute_ecl(data, horizon=horizon)
    return {
        "total_ecl": result.total_ecl,
        "pd_mean": result.pd_mean,
        "horizon_quarters": result.horizon_quarters,
    }


def _batch_discover_handler(**kwargs):
    """Run causal discovery as batch job."""
    engine = OrxaqEngine()
    engine.load_credit_ontology()
    data = engine.generate_synthetic(
        n=kwargs.get("n", 1000),
        seed=kwargs.get("seed", 42),
    )
    variables = kwargs.get("variables")
    if isinstance(variables, str):
        variables = [v.strip() for v in variables.split(",")]
    result = engine.discover_structure(data, variables=variables)
    if hasattr(result, "skeleton"):
        return {
            "skeleton_edges": len(result.skeleton),
            "oriented_edges": len(result.oriented),
        }
    return {
        "edges": len(result.edges),
        "algorithms": list(result.algorithms_used),
    }


def _batch_fairness_handler(**kwargs):
    """Run fairness audit as batch job."""
    engine = OrxaqEngine()
    engine.load_credit_ontology()
    protected = kwargs.get("protected", "age")
    decomp = engine.fairness_audit(protected=protected)
    return {
        "protected_var": decomp.protected_var,
        "total_effect": decomp.total_effect,
        "direct_effect": decomp.direct_effect,
    }


def _cmd_batch_run(args, _engine: OrxaqEngine) -> int:
    """Run a batch job synchronously."""
    q = _get_batch_queue()
    job_type = getattr(args, "job_type", None)
    if not job_type:
        print("Error: job type required (ecl|discover|fairness)")
        return 1
    params: dict = {}
    raw = getattr(args, "params", None)
    if raw:
        params = json.loads(raw)
    jid = q.submit(job_type, params)
    q.process_one()
    job = q.get(jid)
    if _is_json(args):
        return _json_out(job.to_dict())
    if job.status.value == "completed":
        print(f"Job {jid} completed")
        if job.result and job.result.data:
            for k, v in job.result.data.items():
                print(f"  {k}: {v}")
    else:
        print(f"Job {jid} {job.status.value}")
        if job.error:
            print(f"  Error: {job.error}")
    return 0


def _cmd_batch_list(args, _engine: OrxaqEngine) -> int:
    """List all batch jobs."""
    q = _get_batch_queue()
    jobs = q.list_jobs()
    if _is_json(args):
        return _json_out({"jobs": [j.to_dict() for j in jobs]})
    if not jobs:
        print("No batch jobs")
        return 0
    print(f"Batch Jobs ({len(jobs)})")
    for j in jobs:
        line = f"  {j.job_id}  {j.job_type:<12} {j.status.value}"
        print(line)
    return 0


def _cmd_batch_stats(args, _engine: OrxaqEngine) -> int:
    """Show batch queue statistics."""
    q = _get_batch_queue()
    s = q.stats()
    if _is_json(args):
        return _json_out(s)
    print("Batch Queue Stats")
    print(f"  Total jobs:    {s['total_jobs']}")
    for st_key, count in sorted(s["by_status"].items()):
        print(f"  {st_key:<12}   {count}")
    avg = s["avg_duration_ms"]
    print(f"  Avg duration:  {avg:.1f}ms")
    return 0


def _cmd_batch_clear(args, _engine: OrxaqEngine) -> int:
    """Clear completed/failed/cancelled batch jobs."""
    q = _get_batch_queue()
    removed = q.clear()
    if _is_json(args):
        return _json_out({"cleared": removed})
    print(f"Cleared {removed} completed/failed jobs")
    return 0


def _cmd_metrics_snapshot(args, engine: OrxaqEngine) -> int:
    """Dashboard snapshot from MetricsCollector."""
    from orxaq.orchestration.metrics import MetricsCollector

    c = getattr(engine, "_metrics", None)
    if c is None:
        c = MetricsCollector()
    snap = c.dashboard_snapshot()
    if _is_json(args):
        return _json_out(snap)
    print("Metrics Snapshot")
    print(f"  Total tasks:      {snap['total_tasks']}")
    print(f"  Completed:        {snap['completed_tasks']}")
    print(f"  Failed:           {snap['failed_tasks']}")
    print(f"  Error rate:       {snap['error_rate']}")
    tpm = snap["tasks_per_minute"]
    print(f"  Tasks/min:        {tpm:.2f}")
    cost = snap["total_cost_usd"]
    print(f"  Total cost (USD): {cost:.6f}")
    up = snap["uptime_seconds"]
    print(f"  Uptime:           {up:.1f}s")
    return 0


def _cmd_metrics_prometheus(args, engine: OrxaqEngine) -> int:
    """Export Prometheus text exposition format."""
    from orxaq.orchestration.metrics import MetricsCollector
    from orxaq.orchestration.metrics_export import PrometheusExporter

    c = getattr(engine, "_metrics", None)
    if c is None:
        c = MetricsCollector()
    text = PrometheusExporter(c).export()
    if _is_json(args):
        return _json_out({"format": "prometheus", "body": text})
    print(text, end="")
    return 0


def _cmd_metrics_health(args, engine: OrxaqEngine) -> int:
    """Run health check against MetricsCollector."""
    from orxaq.orchestration.metrics import MetricsCollector
    from orxaq.orchestration.metrics_export import HealthCheck

    c = getattr(engine, "_metrics", None)
    if c is None:
        c = MetricsCollector()
    result = HealthCheck(c).check()
    if _is_json(args):
        return _json_out(result)
    print(f"Health: {result['status'].upper()}")
    for name, val in sorted(result["checks"].items()):
        print(f"  {name:<20} {val}")
    up = result["uptime_seconds"]
    print(f"  uptime             {up:.1f}s")
    return 0


def _cmd_metrics_recent(args, engine: OrxaqEngine) -> int:
    """Show recent tasks from MetricsCollector."""
    from orxaq.orchestration.metrics import MetricsCollector

    c = getattr(engine, "_metrics", None)
    if c is None:
        c = MetricsCollector()
    limit = getattr(args, "limit", 20)
    tasks = c.recent_tasks(limit=limit)
    if _is_json(args):
        return _json_out({"tasks": tasks, "count": len(tasks)})
    if not tasks:
        print("No recent tasks")
        return 0
    print(f"Recent Tasks ({len(tasks)})")
    for t in tasks:
        dur = t["duration_ms"]
        print(f"  {t['task_id']:<16} {t['status']:<10} {dur:>8.1f}ms  ${t['cost_usd']:.4f}")
    return 0


# ── Learn subcommand handlers ─────────────────────────────────


def _cmd_learn_summary(args, _engine: OrxaqEngine) -> int:
    from orxaq.intelligence.learning.metrics import MetricCollector

    store = getattr(args, "store", ".orxaq/learning/metrics.json")
    collector = MetricCollector()
    from pathlib import Path

    if Path(store).exists():
        collector.load(store)
    summary = collector.summary()
    if _is_json(args):
        return _json_out(summary)
    if summary["total_runs"] == 0:
        print("Learning Metrics: no runs recorded")
        return 0
    print("Learning Metrics Summary")
    print(f"  Total runs:      {summary['total_runs']}")
    print(f"  Algorithms:      {', '.join(summary['algorithms'])}")
    print(f"  Best SHD:        {summary['best_shd']}")
    print(f"  Best F1:         {summary['best_f1']}")
    print(f"  Avg exec (ms):   {summary['avg_execution_ms']}")
    return 0


def _cmd_learn_experiments(args, _engine: OrxaqEngine) -> int:
    from orxaq.intelligence.learning.experiment import ExperimentTracker

    store = getattr(args, "store", ".orxaq/learning/experiments.json")
    tracker = ExperimentTracker()
    from pathlib import Path

    if Path(store).exists():
        tracker.load(store)
    status_filter = getattr(args, "status", None)
    algo_filter = getattr(args, "algorithm", None)
    tag_filter = getattr(args, "tag", None)
    experiments = tracker.list_experiments(
        status=status_filter,
        algorithm=algo_filter,
        tag=tag_filter,
    )
    if _is_json(args):
        return _json_out({"experiments": [e.to_dict() for e in experiments]})
    if not experiments:
        print("No experiments found")
        return 0
    print(f"Experiments ({len(experiments)})")
    for exp in experiments:
        tags = f" [{', '.join(exp.tags)}]" if exp.tags else ""
        print(f"  {exp.experiment_id}  {exp.name:<20} {exp.algorithm:<12} {exp.status:<10}{tags}")
    return 0


def _cmd_learn_best(args, _engine: OrxaqEngine) -> int:
    from orxaq.intelligence.learning.metrics import MetricCollector

    store = getattr(args, "store", ".orxaq/learning/metrics.json")
    metric = getattr(args, "metric", "edge_f1") or "edge_f1"
    collector = MetricCollector()
    from pathlib import Path

    if Path(store).exists():
        collector.load(store)
    best = collector.best_run(metric=metric)
    if best is None:
        if _is_json(args):
            return _json_out({"best": None, "metric": metric})
        print(f"No runs recorded (metric: {metric})")
        return 0
    if _is_json(args):
        return _json_out({"best": best.to_dict(), "metric": metric})
    print(f"Best Run (by {metric})")
    print(f"  Run ID:         {best.run_id}")
    print(f"  Algorithm:      {best.algorithm}")
    print(f"  SHD:            {best.shd}")
    print(f"  Edge F1:        {best.edge_f1:.4f}")
    print(f"  Edge Precision: {best.edge_precision:.4f}")
    print(f"  Edge Recall:    {best.edge_recall:.4f}")
    print(f"  Orientation:    {best.orientation_accuracy:.4f}")
    print(f"  Exec (ms):      {best.execution_time_ms:.2f}")
    return 0


def _cmd_learn_compare(args, _engine: OrxaqEngine) -> int:
    from orxaq.intelligence.learning.improvement import ImprovementDetector
    from orxaq.intelligence.learning.metrics import MetricCollector

    store = getattr(args, "store", ".orxaq/learning/metrics.json")
    run_a = getattr(args, "run_a", None)
    run_b = getattr(args, "run_b", None)
    if not run_a or not run_b:
        print("Error: --run-a and --run-b are required")
        return 1
    collector = MetricCollector()
    from pathlib import Path

    if Path(store).exists():
        collector.load(store)
    runs_by_id = {r.run_id: r for r in collector.runs}
    if run_a not in runs_by_id:
        print(f"Error: run '{run_a}' not found")
        return 1
    if run_b not in runs_by_id:
        print(f"Error: run '{run_b}' not found")
        return 1
    detector = ImprovementDetector()
    summary = detector.improvement_summary(runs_by_id[run_a], runs_by_id[run_b])
    if _is_json(args):
        return _json_out({"run_a": run_a, "run_b": run_b, **summary})
    print(f"Comparison: {run_a} vs {run_b}")
    if summary["improved"]:
        print("  Improved:")
        for c in summary["improved"]:
            m, p, cur, d = c["metric"], c["previous"], c["current"], c["delta"]
            print(f"    {m}: {p:.4f} -> {cur:.4f} ({d:+.4f})")
    if summary["regressed"]:
        print("  Regressed:")
        for c in summary["regressed"]:
            m, p, cur, d = c["metric"], c["previous"], c["current"], c["delta"]
            print(f"    {m}: {p:.4f} -> {cur:.4f} ({d:+.4f})")
    if summary["unchanged"]:
        print("  Unchanged:")
        for c in summary["unchanged"]:
            print(f"    {c['metric']}: {c['current']:.4f}")
    print(f"  Net improvements: {summary['net_improvements']}")
    return 0


# ── Cost subcommand handlers ─────────────────────────────────


def _cmd_cost_summary(args, _engine: OrxaqEngine) -> int:
    from orxaq.orchestration.providers.cost import CostTracker

    tracker = CostTracker()
    summary = tracker.summary()
    if _is_json(args):
        return _json_out(summary)
    if summary["total_calls"] == 0:
        print("Cost Summary: no usage recorded")
        return 0
    print("Cost Summary")
    print(f"  Total cost:     ${summary['total_cost_usd']:.6f}")
    print(f"  Total calls:    {summary['total_calls']}")
    print(f"  Input tokens:   {summary['total_input_tokens']}")
    print(f"  Output tokens:  {summary['total_output_tokens']}")
    if summary["by_provider"]:
        print("  By provider:")
        for prov, info in sorted(summary["by_provider"].items()):
            print(f"    {prov}: ${info['cost_usd']:.6f} ({info['calls']} calls)")
    return 0


def _cmd_cost_budget(args, _engine: OrxaqEngine) -> int:
    from orxaq.orchestration.providers.cost import BudgetConfig, CostTracker

    hard_limit = getattr(args, "limit", 0.0) or 0.0
    budget = BudgetConfig(hard_limit_usd=hard_limit) if hard_limit > 0 else BudgetConfig()
    tracker = CostTracker(budget=budget)
    status = tracker.check_budget()
    if _is_json(args):
        return _json_out(status)
    print("Budget Status")
    print(f"  Spent (window):   ${status['spent_usd']:.6f}")
    hard_str = f"${status['hard_limit_usd']:.2f}" if status["hard_limit_usd"] > 0 else "unlimited"
    print(f"  Hard limit:       {hard_str}")
    soft_str = f"${status['soft_limit_usd']:.2f}" if status["soft_limit_usd"] > 0 else "none"
    print(f"  Soft limit:       {soft_str}")
    print(f"  Within budget:    {status['within_budget']}")
    print(f"  Warning:          {status['warning']}")
    return 0


def _cmd_cost_pricing(args, _engine: OrxaqEngine) -> int:
    from orxaq.orchestration.providers.cost import DEFAULT_PRICING

    if _is_json(args):
        pricing = {}
        for provider, models in DEFAULT_PRICING.items():
            pricing[provider] = {
                m: {"input_per_1k": v[0], "output_per_1k": v[1]} for m, v in models.items()
            }
        return _json_out({"pricing": pricing})
    print("Default Pricing (per 1K tokens)")
    for provider, models in sorted(DEFAULT_PRICING.items()):
        print(f"  {provider}:")
        for model, (inp, out) in sorted(models.items()):
            print(f"    {model:<20} input=${inp:.5f}  output=${out:.5f}")
    return 0


# ── DB subcommand handlers ────────────────────────────────


_DEFAULT_DB = "orxaq.db"


def _get_db_path(args) -> str:
    """Resolve --db flag or default."""
    return getattr(args, "db_path", None) or _DEFAULT_DB


def _cmd_db_snapshots(args, _engine: OrxaqEngine) -> int:
    """List DAG snapshots."""
    from orxaq.fabric.persistence import SQLiteStore

    with SQLiteStore(_get_db_path(args)) as store:
        snaps = store.list_dag_snapshots()
    if _is_json(args):
        return _json_out({"snapshots": snaps})
    if not snaps:
        print("No snapshots")
        return 0
    print(f"DAG Snapshots ({len(snaps)})")
    for s in snaps:
        print(f"  {s['id']:<6} {s['name']:<20} {s['created_at']}")
    return 0


def _cmd_db_save_snapshot(args, engine: OrxaqEngine) -> int:
    """Save current DAG as snapshot."""
    from orxaq.fabric.persistence import SQLiteStore

    name = getattr(args, "name", "unnamed")
    dag = engine.world_dag
    dag_json = json.dumps(
        {
            "nodes": sorted(dag._entities.keys()),
            "edges": [
                {
                    "source": e.source,
                    "target": e.target,
                    "mechanism": e.mechanism,
                }
                for e in dag._edges.values()
            ],
        }
    )
    with SQLiteStore(_get_db_path(args)) as store:
        sid = store.save_dag_snapshot(name, dag_json)
    if _is_json(args):
        return _json_out({"snapshot_id": sid, "name": name})
    print(f"Saved snapshot {sid}: {name}")
    return 0


def _cmd_db_events(args, _engine: OrxaqEngine) -> int:
    """Query audit events."""
    from orxaq.fabric.persistence import SQLiteStore

    etype = getattr(args, "type", None)
    with SQLiteStore(_get_db_path(args)) as store:
        events = store.query_events(event_type=etype)
    if _is_json(args):
        return _json_out({"events": events})
    if not events:
        print("No events")
        return 0
    print(f"Audit Events ({len(events)})")
    for ev in events:
        det = ev["details"][:40] if ev["details"] else ""
        print(f"  {ev['id']:<6} {ev['event_type']:<12} {ev['actor']:<10} {det}")
    return 0


def _cmd_db_get(args, _engine: OrxaqEngine) -> int:
    """Get value from KV store."""
    from orxaq.fabric.persistence import SQLiteStore

    key = args.key
    with SQLiteStore(_get_db_path(args)) as store:
        val = store.get(key)
    if val is None:
        if _is_json(args):
            return _json_out({"key": key, "value": None})
        print(f"Key not found: {key}")
        return 1
    if _is_json(args):
        return _json_out({"key": key, "value": val})
    print(val)
    return 0


def _cmd_db_put(args, _engine: OrxaqEngine) -> int:
    """Put value in KV store."""
    from orxaq.fabric.persistence import SQLiteStore

    key = args.key
    value = args.value
    with SQLiteStore(_get_db_path(args)) as store:
        store.put(key, value)
    if _is_json(args):
        return _json_out({"key": key, "stored": True})
    print(f"Stored: {key}")
    return 0


# ── Analytics subcommand handlers ────────────────────────────


def _cmd_analytics_summary(args, _engine: OrxaqEngine) -> int:
    """Full analytics summary."""
    from orxaq.fabric.analytics import AnalyticsEngine
    from orxaq.fabric.persistence import SQLiteStore

    with SQLiteStore(_get_db_path(args)) as store:
        ae = AnalyticsEngine(store)
        s = ae.summary()
    if _is_json(args):
        return _json_out(s)
    print(f"Snapshots: {s['snapshots']}")
    print(f"Snapshot names: {', '.join(s['snapshot_names']) or '(none)'}")
    print(f"Audit events: {s['events']}")
    kv = s["kv"]
    print(f"KV keys: {kv['total_keys']} ({kv['total_bytes']} bytes)")
    if kv["prefix_distribution"]:
        for prefix, cnt in kv["prefix_distribution"].items():
            print(f"  {prefix}: {cnt}")
    return 0


def _cmd_analytics_query_events(args, _engine: OrxaqEngine) -> int:
    """Query audit events with filters."""
    from orxaq.fabric.analytics import AnalyticsEngine, QueryFilter
    from orxaq.fabric.persistence import SQLiteStore

    filt = QueryFilter(
        event_types=[args.type] if getattr(args, "type", None) else [],
        actors=[args.actor] if getattr(args, "actor", None) else [],
        start_date=getattr(args, "since", None),
        end_date=getattr(args, "until", None),
        limit=getattr(args, "limit", 50),
    )
    with SQLiteStore(_get_db_path(args)) as store:
        ae = AnalyticsEngine(store)
        events = ae.query_events(filt)
    if _is_json(args):
        return _json_out({"events": events, "count": len(events)})
    if not events:
        print("No events found.")
        return 0
    for ev in events:
        print(f"[{ev['created_at']}] {ev['event_type']} by {ev['actor']}: {ev['details']}")
    return 0


def _cmd_analytics_query_snapshots(args, _engine: OrxaqEngine) -> int:
    """Query DAG snapshots with filters."""
    from orxaq.fabric.analytics import AnalyticsEngine, QueryFilter
    from orxaq.fabric.persistence import SQLiteStore

    filt = QueryFilter(
        name_pattern=getattr(args, "name", None),
        start_date=getattr(args, "since", None),
        end_date=getattr(args, "until", None),
        limit=getattr(args, "limit", 50),
    )
    with SQLiteStore(_get_db_path(args)) as store:
        ae = AnalyticsEngine(store)
        snaps = ae.query_snapshots(filt)
    if _is_json(args):
        return _json_out({"snapshots": snaps, "count": len(snaps)})
    if not snaps:
        print("No snapshots found.")
        return 0
    for s in snaps:
        meta = s["metadata"] or {}
        print(f"[{s['id']}] {s['name']} @ {s['created_at']} {meta}")
    return 0


def _cmd_analytics_aggregate(args, _engine: OrxaqEngine) -> int:
    """Aggregate audit events by type or actor."""
    from orxaq.fabric.analytics import AnalyticsEngine, QueryFilter
    from orxaq.fabric.persistence import SQLiteStore

    group_by = getattr(args, "group_by", "event_type")
    filt = QueryFilter(
        start_date=getattr(args, "since", None),
        end_date=getattr(args, "until", None),
    )
    with SQLiteStore(_get_db_path(args)) as store:
        ae = AnalyticsEngine(store)
        result = ae.aggregate_events(group_by, filt)
    if _is_json(args):
        return _json_out(result.to_dict())
    print(f"Total: {result.count}")
    for grp, cnt in result.groups.items():
        print(f"  {grp}: {cnt}")
    return 0


def _cmd_analytics_timeline(args, _engine: OrxaqEngine) -> int:
    """Time-series rollup of audit events."""
    from orxaq.fabric.analytics import AnalyticsEngine, QueryFilter
    from orxaq.fabric.persistence import SQLiteStore

    granularity = getattr(args, "granularity", "day")
    filt = QueryFilter(
        start_date=getattr(args, "since", None),
        end_date=getattr(args, "until", None),
    )
    with SQLiteStore(_get_db_path(args)) as store:
        ae = AnalyticsEngine(store)
        buckets = ae.event_timeline(granularity, filt)
    if _is_json(args):
        return _json_out({"buckets": [b.to_dict() for b in buckets]})
    if not buckets:
        print("No data.")
        return 0
    for b in buckets:
        bar = "#" * b.count
        print(f"{b.period}  {b.count:>4}  {bar}")
    return 0


def _cmd_analytics_search(args, _engine: OrxaqEngine) -> int:
    """Full-text search in audit event details."""
    from orxaq.fabric.analytics import AnalyticsEngine
    from orxaq.fabric.persistence import SQLiteStore

    query = args.query
    with SQLiteStore(_get_db_path(args)) as store:
        ae = AnalyticsEngine(store)
        results = ae.search_events(query, limit=50)
    if _is_json(args):
        return _json_out({"results": results, "count": len(results)})
    if not results:
        print("No matches.")
        return 0
    for ev in results:
        print(f"[{ev['created_at']}] {ev['event_type']} by {ev['actor']}: {ev['details']}")
    return 0


# ── Stream subcommand handlers ───────────────────────────────


def _cmd_stream_ecl(args, engine: OrxaqEngine) -> int:
    """Stream ECL computation results."""
    import sys as _sys

    from orxaq.fabric.streaming import (
        paginate,
        stream_ndjson,
        stream_sse,
    )

    _ensure_ontology(engine)
    data = _load_data(args, engine)
    ecl_result = engine.compute_ecl(data)

    records: list[dict] = []
    if hasattr(ecl_result, "ecl_by_segment"):
        for seg, val in ecl_result.ecl_by_segment.items():
            records.append({"segment": seg, "ecl": round(val, 6)})
        records.append(
            {
                "segment": "TOTAL",
                "ecl": round(ecl_result.total_ecl, 6),
            }
        )

    fmt = getattr(args, "fmt", "ndjson")
    page = getattr(args, "page", 0)

    if page > 0:
        result = paginate(
            records,
            page=page,
            page_size=getattr(args, "page_size", 50),
        )
        if _is_json(args) or fmt == "json":
            return _json_out(result)
        for r in result["records"]:
            print(f"{r['segment']}: {r['ecl']}")
        print(f"Page {result['page']}/{result['total_pages']} ({result['total_records']} total)")
        return 0

    if fmt == "sse":
        events = stream_sse(records, total=len(records))
        for ev in events:
            _sys.stdout.write(ev + "\n")
        return 0

    if fmt == "json":
        return _json_out({"records": records, "count": len(records)})

    # Default: ndjson
    stream_ndjson(records, output=_sys.stdout, total=len(records))
    return 0


def _cmd_stream_snapshots(args, _engine: OrxaqEngine) -> int:
    """Stream snapshot list from persistence."""
    import sys as _sys

    from orxaq.fabric.persistence import SQLiteStore
    from orxaq.fabric.streaming import stream_ndjson, stream_sse

    with SQLiteStore(_get_db_path(args)) as store:
        snaps = store.list_dag_snapshots(limit=1000)

    fmt = getattr(args, "fmt", "ndjson")

    if fmt == "sse":
        events = stream_sse(snaps, total=len(snaps))
        for ev in events:
            _sys.stdout.write(ev + "\n")
        return 0

    if fmt == "json":
        return _json_out({"snapshots": snaps, "count": len(snaps)})

    stream_ndjson(snaps, output=_sys.stdout, total=len(snaps))
    return 0


# ── Audit-report subcommand handlers ─────────────────────────


def _cmd_audit_report_full(args, engine):
    """Generate full audit trail report."""
    from orxaq.fabric.audit_report import AuditFilter, AuditReportGenerator

    gen = AuditReportGenerator(audit_log=engine.audit_log)
    filt = AuditFilter(
        actions=[a] if (a := getattr(args, "action", None)) else [],
        actors=[a] if (a := getattr(args, "actor", None)) else [],
        keyword=getattr(args, "keyword", None),
        limit=getattr(args, "limit", 1000),
    )
    report = gen.full_report(filt=filt)
    if _is_json(args):
        return _json_out(report.to_dict())
    print(f"=== {report.title} ===")
    print(f"Generated: {report.generated_at}")
    s = report.summary
    print(f"Events: {s.total_events}  Actors: {s.unique_actors}  Actions: {s.unique_actions}")
    if report.integrity:
        status = "VALID" if report.integrity.valid else "INVALID"
        print(f"Chain integrity: {status} ({report.integrity.checked_entries} entries)")
    for e in report.entries[:20]:
        print(f"  [{e['iso_time']}] {e['action']} by {e['actor']}: {e['details']}")
    if report.entry_count > 20:
        print(f"  ... and {report.entry_count - 20} more")
    return 0


def _cmd_audit_report_compliance(args, engine):
    """Generate compliance-focused audit report."""
    from orxaq.fabric.audit_report import AuditReportGenerator

    framework = getattr(args, "framework", "sr11-7")
    gen = AuditReportGenerator(audit_log=engine.audit_log)
    report = gen.compliance_report(framework=framework)
    if _is_json(args):
        return _json_out(report.to_dict())
    print(f"=== {report.title} ===")
    s = report.summary
    print(f"Events: {s.total_events}  Actors: {s.unique_actors}")
    if report.integrity:
        status = "VALID" if report.integrity.valid else "INVALID"
        print(f"Chain integrity: {status}")
    return 0


def _cmd_audit_report_verify(args, engine):
    """Verify audit chain integrity."""
    from orxaq.fabric.audit_report import AuditReportGenerator

    gen = AuditReportGenerator(audit_log=engine.audit_log)
    result = gen.verify_integrity()
    if _is_json(args):
        return _json_out(result.to_dict())
    status = "VALID" if result.valid else "INVALID"
    print(f"Integrity: {status}")
    print(f"  Total entries: {result.total_entries}")
    print(f"  Checked: {result.checked_entries}")
    if not result.valid:
        print(f"  Error: {result.error_message}")
    return 0


def _cmd_audit_report_actor(args, engine):
    """Generate report for a specific actor."""
    from orxaq.fabric.audit_report import AuditReportGenerator

    actor = getattr(args, "actor_name", "")
    gen = AuditReportGenerator(audit_log=engine.audit_log)
    report = gen.actor_report(actor)
    if _is_json(args):
        return _json_out(report.to_dict())
    print(f"=== {report.title} ===")
    print(f"Events: {report.entry_count}")
    for e in report.entries[:20]:
        print(f"  [{e['iso_time']}] {e['action']}: {e['details']}")
    return 0


# ── Bulk subcommand handlers ─────────────────────────────────


def _cmd_bulk_run(args, _engine):
    """Run bulk processing on a JSON-lines input file."""
    import json
    import sys as _sys

    from orxaq.orchestration.bulk import BulkProcessor

    input_path = getattr(args, "input", None)
    if not input_path:
        print("Error: --input is required")
        return 1

    try:
        with open(input_path) as f:
            records = [json.loads(line) for line in f if line.strip()]
    except (FileNotFoundError, json.JSONDecodeError) as e:
        print(f"Error reading input: {e}")
        return 1

    chunk_size = getattr(args, "chunk_size", 100)
    max_errors = getattr(args, "max_errors", 0)
    proc = BulkProcessor(chunk_size=chunk_size, max_errors=max_errors)

    def passthrough(record):
        return record

    n_workers = getattr(args, "workers", 0)
    if n_workers > 1:
        result = proc.run_threaded(records, passthrough, n_workers=n_workers)
    else:
        result = proc.run(records, passthrough)

    if _is_json(args):
        return _json_out(result.to_dict())
    print(f"Processed: {result.progress.processed}")
    print(f"  Succeeded: {result.progress.succeeded}  Failed: {result.progress.failed}")
    print(f"  Success rate: {result.success_rate:.1%}")
    if result.errors:
        for err in result.errors[:5]:
            _sys.stderr.write(f"  Error at index {err['index']}: {err['error']}\n")
    return 0


def _cmd_bulk_dry_run(args, _engine):
    """Dry-run bulk processing to estimate timing and errors."""
    import json

    from orxaq.orchestration.bulk import BulkProcessor

    input_path = getattr(args, "input", None)
    if not input_path:
        print("Error: --input is required")
        return 1

    try:
        with open(input_path) as f:
            records = [json.loads(line) for line in f if line.strip()]
    except (FileNotFoundError, json.JSONDecodeError) as e:
        print(f"Error reading input: {e}")
        return 1

    sample_size = getattr(args, "sample_size", 10)
    proc = BulkProcessor()

    def passthrough(record):
        return record

    report = proc.dry_run(records, passthrough, sample_size=sample_size)

    if _is_json(args):
        return _json_out(report)
    print(f"Dry run: {report['sample_size']} of {report['total_records']} records")
    print(f"  Successes: {report['sample_successes']}  Errors: {report['sample_errors']}")
    print(f"  Avg time/record: {report['avg_time_per_record_ms']:.2f}ms")
    print(f"  Est. total time: {report['estimated_total_time_s']:.2f}s")
    print(f"  Est. error rate: {report['estimated_error_rate']:.2%}")
    return 0


# ── Run subcommand handler ────────────────────────────────────


def _cmd_run(args, _engine: OrxaqEngine) -> int:
    """Run full CECL/CCAR filing pipeline end-to-end."""
    from orxaq.orchestration.run import RunConfig, run_pipeline  # noqa: PLC0415

    cfg = RunConfig(
        institution_id=getattr(args, "institution", "BANK001"),
        period=getattr(args, "period", "2026Q1"),
        output_dir=getattr(args, "output", "./filing"),
        n_loans=getattr(args, "loans", 200),
        seed=getattr(args, "seed", 42),
        include_fair_lending=not getattr(args, "no_fair_lending", False),
        input_file=getattr(args, "input_file", None),
    )
    try:
        result = run_pipeline(cfg)
    except Exception as exc:  # noqa: BLE001
        print(f"ERROR: {exc}", file=__import__("sys").stderr)
        return 1

    if _is_json(args):
        return _json_out(
            {
                "institution_id": result.institution_id,
                "period": result.period,
                "output_dir": result.output_dir,
                "ecl_total": result.ecl_total,
                "files": result.files,
                "duration_s": round(result.duration_s, 3),
                "audit_entries": result.audit_entries,
            }
        )
    print("Filing run complete")
    print(f"  Institution:  {result.institution_id}")
    print(f"  Output:       {result.output_dir}")
    print(f"  ECL Total:    {result.ecl_total:,.4f}")
    print(f"  Files:        {len(result.files)}")
    print(f"  Duration:     {result.duration_s:.2f}s")
    print(f"  Audit:        {result.audit_entries} entries")
    return 0


# ── Filing subcommand handlers ────────────────────────────────


def _cmd_filing_generate(args, _engine: OrxaqEngine) -> int:
    """Generate full CECL/CCAR regulatory filing package."""
    from orxaq.credit.filing import FilingConfig, FilingGenerator

    cfg = FilingConfig(
        institution_id=getattr(args, "institution_id", "BANK001"),
        period=getattr(args, "period", "2026Q1"),
        output_dir=getattr(args, "output_dir", "."),
        seed=getattr(args, "seed", 42),
        n_samples=getattr(args, "n_samples", 500),
        horizon_quarters=getattr(args, "horizon_quarters", 9),
    )
    result = FilingGenerator(cfg).generate()
    if _is_json(args):
        return _json_out(
            {
                "output_dir": result.output_dir,
                "institution_id": result.institution_id,
                "period": result.period,
                "duration_seconds": round(result.duration_seconds, 3),
                "audit_entries_count": result.audit_entries_count,
                "files": result.files,
            }
        )
    print(f"Filing generated: {result.output_dir}")
    print(f"  Institution: {result.institution_id}  Period: {result.period}")
    print(f"  Duration: {result.duration_seconds:.2f}s  Files: {len(result.files)}")
    return 0


def _format_validate_report(result: dict, output_dir: str = "") -> str:
    """Format a validate_filing_package result as human-readable text."""
    lines: list[str] = []
    status_sym = "PASS" if result["status"] == "pass" else "FAIL"
    passed = sum(1 for c in result["checks"] if c["passed"])
    total = len(result["checks"])
    header = f"Filing Validation: {status_sym} ({passed}/{total} checks passed)"
    if output_dir:
        header += f"  [{output_dir}]"
    lines.append(header)
    lines.append(f"Files verified: {result['files_verified']}  Generated: {result['generated_at']}")
    lines.append("")
    for chk in result["checks"]:
        sym = "[OK]" if chk["passed"] else "[FAIL]"
        lines.append(f"  {sym}  {chk['name']}: {chk['detail']}")
    return "\n".join(lines)


def _cmd_filing_validate(args, _engine: OrxaqEngine) -> int:
    """Re-verify a previously generated filing package."""
    from orxaq.credit.filing_validate import cmd_filing_validate

    output_dir = getattr(args, "output_dir", ".")
    result = cmd_filing_validate(output_dir)
    if _is_json(args):
        print(json.dumps(result, indent=2, default=str))
    else:
        print(_format_validate_report(result, output_dir=output_dir))
    return 0 if result["status"] == "pass" else 1


def _cmd_demo(args, _engine: OrxaqEngine) -> int:
    """Run bank-facing regulatory demo: full CECL/CCAR filing in one command."""
    from orxaq.experiences.demo import run_regulatory_demo

    sub = getattr(args, "demo_cmd", None)
    if sub != "regulatory":
        print("Usage: orxaq demo regulatory [--output-dir DIR] [--institution-id ID]")
        print("       [--period PERIOD] [--seed N] [--json]")
        return 1

    result = run_regulatory_demo(
        output_dir=getattr(args, "output_dir", "./demo_output"),
        institution_id=getattr(args, "institution_id", "DEMO_BANK_001"),
        period=getattr(args, "period", "2025Q4"),
        seed=getattr(args, "seed", 42),
    )

    if _is_json(args):
        return _json_out(result)

    status_sym = "PASS" if result["compliance_status"] == "PASS" else "FAIL"
    print(f"Orxaq Regulatory Demo — {result['institution_id']}  {result['period']}")
    print(f"Output directory: {result['output_dir']}")
    print(f"Compliance status: {status_sym}  Duration: {result['duration_seconds']:.2f}s")
    print(f"Audit entries: {result['audit_entries']}  Files generated: {len(result['files'])}")
    print("")
    for path in result["files"]:
        print(f"  {path}")
    return 0


# ── Parser construction ──────────────────────────────────────


def build_parser() -> argparse.ArgumentParser:
    """Build the CLI argument parser."""
    parser = argparse.ArgumentParser(
        prog="orxaq",
        description="Orxaq — Cognitive Causal Operating System for Credit Risk",
    )
    parser.add_argument("--json", dest="json_output", action="store_true", help="Output JSON")
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # ── status ──
    subparsers.add_parser("status", help="Show engine status")

    # ── credit ──
    credit_parser = subparsers.add_parser("credit", help="Credit risk operations")
    credit_sub = credit_parser.add_subparsers(dest="credit_cmd", help="Credit subcommands")

    onto = credit_sub.add_parser("ontology", help="Show/export credit ontology")
    onto.add_argument("--show", action="store_true", default=True, help="Show variables")
    onto.add_argument("--export", type=str, help="Export to JSON file")

    pd_parser = credit_sub.add_parser("pd", help="Predict probability of default")
    pd_parser.add_argument("--data", type=str, help="Data file (CSV/JSON)")
    pd_parser.add_argument("--scenario", type=str, help="Stress scenario name")
    pd_parser.add_argument("--borrower", type=str, help="Borrower JSON for single prediction")

    ecl_parser = credit_sub.add_parser("ecl", help="Compute CECL expected credit loss")
    ecl_parser.add_argument("--data", type=str, help="Data file (CSV/JSON)")
    ecl_parser.add_argument("--horizon", type=str, help="Forecast horizon (N or 'dynamic')")

    fair_parser = credit_sub.add_parser("fairness", help="Fair lending analysis")
    fair_parser.add_argument("--data", type=str, help="Data file (CSV/JSON)")
    fair_parser.add_argument("--protected", type=str, default="age", help="Protected variable")

    port_parser = credit_sub.add_parser("portfolio", help="Portfolio risk analytics")
    port_parser.add_argument("--data", type=str, help="Data file (CSV/JSON)")
    port_parser.add_argument("--vintage", action="store_true", help="Include vintage breakdown")

    stress_parser = credit_sub.add_parser("stress", help="Stress test portfolio")
    stress_parser.add_argument("--data", type=str, help="Data file (CSV/JSON)")
    stress_parser.add_argument(
        "--scenario",
        type=str,
        default="adverse",
        help="Scenario (baseline|adverse|severely_adverse)",
    )

    rpt_parser = credit_sub.add_parser("report", help="Generate full risk report")
    rpt_parser.add_argument("--data", type=str, help="Data file (CSV/JSON)")
    rpt_parser.add_argument(
        "--scenario",
        type=str,
        default="adverse",
        help="Stress scenario (baseline|adverse|severely_adverse)",
    )
    rpt_parser.add_argument("--protected", type=str, default="age", help="Protected variable")
    rpt_parser.add_argument("--output", type=str, help="Save report JSON to file")
    cmp_parser = credit_sub.add_parser(
        "compare", help="Compare stress scenarios with delta analytics"
    )
    cmp_parser.add_argument(
        "--scenarios",
        type=str,
        default="baseline,adverse,severely_adverse",
        help="Comma-separated scenario names (default: all three)",
    )
    cmp_parser.add_argument(
        "--format",
        dest="format",
        choices=["json", "text"],
        default="text",
        help="Output format (json|text)",
    )
    cmp_parser.add_argument(
        "--starting-cet1",
        dest="starting_cet1",
        type=float,
        default=0.12,
        help="Starting CET1 capital ratio (default: 0.12)",
    )
    cmp_parser.add_argument("--output", type=str, help="Write output to file path")

    exp_reg_parser = credit_sub.add_parser(
        "export-regulatory", help="Export CECL/CCAR regulatory submission artifact"
    )
    exp_reg_parser.add_argument(
        "--framework",
        choices=["cecl", "ccar"],
        default="cecl",
        help="Regulatory framework (cecl|ccar), default: cecl",
    )
    exp_reg_parser.add_argument(
        "--format",
        dest="format",
        choices=["json", "markdown"],
        default="json",
        help="Output format",
    )
    exp_reg_parser.add_argument(
        "--institution-id",
        dest="institution_id",
        type=str,
        default="ORXAQ-DEMO-001",
        help="Institution identifier",
    )
    exp_reg_parser.add_argument(
        "--reporting-period",
        dest="reporting_period",
        type=str,
        default="2024-Q4",
        help="Reporting period (e.g. 2024-Q4)",
    )
    exp_reg_parser.add_argument("--output", type=str, help="Write output to file")
    exp_reg_parser.add_argument(
        "--scenarios",
        nargs="+",
        choices=["baseline", "adverse", "severely_adverse"],
        help="CCAR scenarios (default: all three)",
    )

    reg_parser = credit_sub.add_parser(
        "regulatory-export", help="Generate FR Y-14A / CCAR regulatory submission package"
    )
    reg_parser.add_argument(
        "--institution-id",
        dest="institution_id",
        type=str,
        required=False,
        default="ORG001",
        help="Regulatory institution ID",
    )
    reg_parser.add_argument(
        "--as-of",
        dest="as_of",
        type=str,
        default="2025-12-31",
        help="Reporting as-of date (YYYY-MM-DD)",
    )
    reg_parser.add_argument(
        "--period", type=str, default="2025Q4", help="Submission period (e.g. 2025Q4)"
    )
    reg_parser.add_argument(
        "--format", type=str, default="json", choices=["json", "csv"], help="Output format"
    )
    reg_parser.add_argument("--data", type=str, help="Data file (CSV/JSON)")
    reg_parser.add_argument("--output", type=str, help="Write output to file")

    rpt_reg_parser = credit_sub.add_parser(
        "regulatory-report",
        help="Generate CECL/CCAR/DFAST regulatory HTML/Markdown report",
    )
    rpt_reg_parser.add_argument(
        "--institution",
        type=str,
        default="Orxaq Financial, N.A.",
        help="Institution name for report header",
    )
    rpt_reg_parser.add_argument(
        "--scenario",
        type=str,
        default="severely_adverse",
        choices=["baseline", "adverse", "severely_adverse"],
        help="CCAR stress scenario",
    )
    rpt_reg_parser.add_argument(
        "--format",
        type=str,
        default="html",
        choices=["html", "markdown"],
        help="Report output format",
    )
    rpt_reg_parser.add_argument("--data", type=str, help="Data file (CSV/JSON)")
    rpt_reg_parser.add_argument("--output", type=str, help="Save report to file")

    # ── discover ──
    disc = subparsers.add_parser("discover", help="Discover causal structure from data")
    disc.add_argument("--data", type=str, help="Data file (CSV/JSON)")
    disc.add_argument("--alpha", type=float, default=0.05, help="Significance level")
    disc.add_argument("--consensus", action="store_true", help="Use multi-algorithm consensus")
    disc.add_argument("--variables", type=str, help="Comma-separated variable names")

    # ── validate ──
    val = subparsers.add_parser("validate", help="Validate DAG against data")
    val.add_argument("--data", type=str, help="Data file (CSV/JSON)")
    val.add_argument("--alpha", type=float, default=0.05, help="Significance level")
    val.add_argument(
        "--ground-truth", type=str, help="Ground truth DAG JSON for pipeline validation"
    )
    val.add_argument(
        "--algorithm", type=str, default="pc", help="Discovery algorithm (pc|consensus)"
    )
    val.add_argument("--output", type=str, help="Save validation report to file")
    val.add_argument(
        "--generate", action="store_true", help="Generate synthetic validation dataset"
    )
    val.add_argument("--rows", type=int, default=50000, help="Rows to generate (with --generate)")
    val.add_argument("--seed", type=int, default=42, help="Random seed")

    # ── fabric ──
    fabric_parser = subparsers.add_parser("fabric", help="Data fabric operations")
    fabric_sub = fabric_parser.add_subparsers(dest="fabric_cmd", help="Fabric subcommands")

    ing = fabric_sub.add_parser("ingest", help="Ingest a data file")
    ing.add_argument("file", type=str, help="File to ingest")
    ing.add_argument("--schema", type=str, help="Schema name (e.g., 'credit')")

    prof = fabric_sub.add_parser("profile", help="Profile data statistics")
    prof.add_argument("--data", type=str, help="Data file (CSV/JSON)")

    qual = fabric_sub.add_parser("quality", help="Run quality gates")
    qual.add_argument("--data", type=str, help="Data file (CSV/JSON)")
    qual.add_argument("--gates", type=str, help="Comma-separated gate names")

    # ── audit ──
    audit = subparsers.add_parser("audit", help="Show audit log")
    audit.add_argument("--last", type=int, default=20, help="Number of recent entries")
    audit.add_argument("--action", type=str, help="Filter by action")
    audit.add_argument("--actor", type=str, help="Filter by actor")

    # ── serve ──
    srv = subparsers.add_parser("serve", help="Start Observatory dashboard")
    srv.add_argument("--port", type=int, default=8741, help="Server port")
    srv.add_argument(
        "--skin",
        type=str,
        default="gamma",
        choices=["gamma", "beta", "alpha"],
        help="Visual skin (gamma|beta|alpha)",
    )

    # ── edit ──
    edit_parser = subparsers.add_parser("edit", help="Open DAG editor in browser")
    edit_parser.add_argument("--port", type=int, default=8742, help="Server port")
    edit_parser.add_argument(
        "--skin",
        type=str,
        default="gamma",
        choices=["gamma", "beta", "alpha"],
        help="Visual skin (gamma|beta|alpha)",
    )

    # ── simulate ──
    sim_parser = subparsers.add_parser("simulate", help="Run scenario simulation")
    sim_parser.add_argument(
        "--scenario",
        type=str,
        default="adverse",
        help="Scenario name (baseline|adverse|severely_adverse) or custom",
    )
    sim_parser.add_argument("--file", type=str, help="Custom scenario JSON file")
    sim_parser.add_argument("--seed", type=int, default=42, help="Random seed")
    sim_parser.add_argument(
        "--monte-carlo", type=int, default=0, help="Number of Monte Carlo runs (0=disabled)"
    )

    # ── providers ──
    prov_parser = subparsers.add_parser("providers", help="LLM provider operations")
    prov_sub = prov_parser.add_subparsers(dest="providers_cmd", help="Provider subcommands")
    prov_sub.add_parser("check", help="Health-check all configured providers")
    prov_sub.add_parser("list", help="List available models across all providers")
    cost_parser = prov_sub.add_parser("cost", help="Show provider cost summary")
    cost_parser.add_argument("--budget", type=float, default=0, help="Set hard budget limit in USD")
    prov_sub.add_parser("cache", help="Show response cache statistics")
    prov_sub.add_parser("stats", help="Show routing statistics")

    # ── model ──
    model_parser = subparsers.add_parser("model", help="Model checkpoint operations")
    model_sub = model_parser.add_subparsers(dest="model_cmd", help="Model subcommands")

    model_save = model_sub.add_parser("save", help="Checkpoint current DAG state")
    model_save.add_argument("--name", type=str, required=True, help="Model name")
    model_save.add_argument("--version", type=str, default="1.0.0", help="Version string")
    model_save.add_argument(
        "--store", type=str, default=".orxaq/checkpoints", help="Checkpoint store directory"
    )

    model_load = model_sub.add_parser("load", help="Restore DAG from checkpoint")
    model_load.add_argument("--name", type=str, required=True, help="Model name")
    model_load.add_argument("--version", type=str, help="Version (default: latest)")
    model_load.add_argument(
        "--store", type=str, default=".orxaq/checkpoints", help="Checkpoint store directory"
    )

    model_list = model_sub.add_parser("list", help="List saved models")
    model_list.add_argument(
        "--store", type=str, default=".orxaq/checkpoints", help="Checkpoint store directory"
    )

    model_versions = model_sub.add_parser("versions", help="List versions of a model")
    model_versions.add_argument("--name", type=str, required=True, help="Model name")
    model_versions.add_argument(
        "--store", type=str, default=".orxaq/checkpoints", help="Checkpoint store directory"
    )

    model_diff = model_sub.add_parser("diff", help="Compare two model versions")
    model_diff.add_argument("--name", type=str, required=True, help="Model name")
    model_diff.add_argument("--v1", type=str, required=True, help="First version")
    model_diff.add_argument("--v2", type=str, required=True, help="Second version")
    model_diff.add_argument(
        "--store", type=str, default=".orxaq/checkpoints", help="Checkpoint store directory"
    )

    model_regression = model_sub.add_parser(
        "regression", help="Run regression test between two versions"
    )
    model_regression.add_argument("--name", type=str, required=True, help="Model name")
    model_regression.add_argument("--v1", type=str, required=True, help="Baseline version")
    model_regression.add_argument("--v2", type=str, required=True, help="Candidate version")
    model_regression.add_argument(
        "--store", type=str, default=".orxaq/checkpoints", help="Checkpoint store directory"
    )

    model_metrics_p = model_sub.add_parser("metrics", help="Show DAG metrics for a model version")
    model_metrics_p.add_argument("--name", type=str, required=True, help="Model name")
    model_metrics_p.add_argument(
        "--version", type=str, default=None, help="Version (default: latest)"
    )
    model_metrics_p.add_argument(
        "--store", type=str, default=".orxaq/checkpoints", help="Checkpoint store directory"
    )

    # ── export ──
    export_parser = subparsers.add_parser("export", help="Export data and results")
    export_sub = export_parser.add_subparsers(dest="export_cmd", help="Export subcommands")

    exp_dag = export_sub.add_parser("dag", help="Export DAG structure")
    exp_dag.add_argument(
        "--format",
        type=str,
        default="json",
        choices=["json", "csv", "dot", "mermaid"],
        help="Output format",
    )
    exp_dag.add_argument("--output", type=str, help="Output file path")

    exp_ecl = export_sub.add_parser("ecl", help="Export ECL results")
    exp_ecl.add_argument(
        "--format", type=str, default="json", choices=["json", "csv"], help="Output format"
    )
    exp_ecl.add_argument("--output", type=str, help="Output file path")

    exp_edges = export_sub.add_parser("edges", help="Export DAG edges as CSV")
    exp_edges.add_argument("--output", type=str, help="Output file path")

    exp_nodes = export_sub.add_parser("nodes", help="Export DAG nodes as CSV")
    exp_nodes.add_argument("--output", type=str, help="Output file path")

    # ── learn ──
    learn_parser = subparsers.add_parser("learn", help="Learning metrics and experiments")
    learn_sub = learn_parser.add_subparsers(dest="learn_cmd", help="Learn subcommands")

    learn_summary = learn_sub.add_parser("summary", help="Show learning metrics summary")
    learn_summary.add_argument(
        "--store",
        type=str,
        default=".orxaq/learning/metrics.json",
        help="Metrics JSON store path",
    )

    learn_exp = learn_sub.add_parser("experiments", help="List experiments")
    learn_exp.add_argument(
        "--store",
        type=str,
        default=".orxaq/learning/experiments.json",
        help="Experiments JSON store path",
    )
    learn_exp.add_argument("--status", type=str, help="Filter by status")
    learn_exp.add_argument("--algorithm", type=str, help="Filter by algorithm")
    learn_exp.add_argument("--tag", type=str, help="Filter by tag")

    learn_best = learn_sub.add_parser("best", help="Show best run by metric")
    learn_best.add_argument(
        "--store",
        type=str,
        default=".orxaq/learning/metrics.json",
        help="Metrics JSON store path",
    )
    learn_best.add_argument(
        "--metric",
        type=str,
        default="edge_f1",
        help="Metric to rank by",
    )

    learn_cmp = learn_sub.add_parser("compare", help="Compare two runs")
    learn_cmp.add_argument(
        "--store",
        type=str,
        default=".orxaq/learning/metrics.json",
        help="Metrics JSON store path",
    )
    learn_cmp.add_argument("--run-a", type=str, help="First run ID")
    learn_cmp.add_argument("--run-b", type=str, help="Second run ID")

    # ── cost ──
    cost_top = subparsers.add_parser("cost", help="Cost tracking and budget management")
    cost_sub = cost_top.add_subparsers(dest="cost_cmd", help="Cost subcommands")

    cost_sub.add_parser("status", help="Show current budget status and spend")
    cost_set = cost_sub.add_parser("set", help="Set monthly budget limit")
    cost_set.add_argument("budget_amount", type=float, help="Monthly budget in USD")
    cost_sub.add_parser("summary", help="Show cost summary")

    cost_budget = cost_sub.add_parser("budget", help="Check budget status")
    cost_budget.add_argument(
        "--limit",
        type=float,
        default=0.0,
        help="Hard budget limit in USD",
    )

    cost_sub.add_parser("pricing", help="Show default pricing table")

    # ── tenant ──
    tenant_top = subparsers.add_parser("tenant", help="Multi-tenant management")
    tenant_sub = tenant_top.add_subparsers(dest="tenant_cmd", help="Tenant subcommands")
    t_create = tenant_sub.add_parser("create", help="Create a new tenant")
    t_create.add_argument("tenant_id", type=str, help="Unique tenant identifier")
    t_create.add_argument("--name", type=str, default="", help="Display name")
    t_create.add_argument("--tier", type=str, default="free", help="Tier (free|pro|enterprise)")
    t_create.add_argument("--max-entities", type=int, default=100, help="Max DAG entities")
    t_create.add_argument("--max-edges", type=int, default=500, help="Max DAG edges")
    tenant_sub.add_parser("list", help="List all tenants")
    t_get = tenant_sub.add_parser("get", help="Get tenant details")
    t_get.add_argument("tenant_id", type=str, help="Tenant ID")
    t_del = tenant_sub.add_parser("delete", help="Remove a tenant")
    t_del.add_argument("tenant_id", type=str, help="Tenant ID")

    # ── batch ──
    batch_top = subparsers.add_parser("batch", help="Batch job processing")
    batch_sub = batch_top.add_subparsers(dest="batch_cmd", help="Batch subcommands")
    b_run = batch_sub.add_parser("run", help="Run a batch job")
    b_run.add_argument(
        "job_type",
        type=str,
        choices=["ecl", "discover", "fairness"],
        help="Job type to run",
    )
    b_run.add_argument(
        "--params",
        type=str,
        default=None,
        help="JSON params string",
    )
    batch_sub.add_parser("list", help="List batch jobs")
    batch_sub.add_parser("stats", help="Show queue statistics")
    batch_sub.add_parser("clear", help="Clear completed jobs")

    # ── Metrics subcommands ──────────────────────────────────
    metrics_top = subparsers.add_parser("metrics", help="Observability metrics and health")
    metrics_sub = metrics_top.add_subparsers(dest="metrics_cmd", help="Metrics subcommands")
    metrics_sub.add_parser("snapshot", help="Dashboard metrics snapshot")
    metrics_sub.add_parser("prometheus", help="Prometheus text exposition")
    metrics_sub.add_parser("health", help="System health check")
    m_recent = metrics_sub.add_parser("recent", help="Recent tasks")
    m_recent.add_argument(
        "--limit",
        type=int,
        default=20,
        help="Max tasks to show (default: 20)",
    )

    # ── db ──
    db_top = subparsers.add_parser("db", help="SQLite persistence operations")
    db_top.add_argument(
        "--db",
        dest="db_path",
        type=str,
        help="Path to SQLite DB file",
    )
    db_sub = db_top.add_subparsers(dest="db_cmd", help="DB subcommands")
    db_sub.add_parser("snapshots", help="List DAG snapshots")
    db_save = db_sub.add_parser("save-snapshot", help="Save current DAG")
    db_save.add_argument(
        "--name",
        type=str,
        default="unnamed",
        help="Snapshot name",
    )
    db_ev = db_sub.add_parser("events", help="Query audit events")
    db_ev.add_argument(
        "--type",
        type=str,
        help="Filter by event type",
    )
    db_get = db_sub.add_parser("get", help="Get value from KV store")
    db_get.add_argument("key", help="Key to look up")
    db_put = db_sub.add_parser("put", help="Put value in KV store")
    db_put.add_argument("key", help="Key to store")
    db_put.add_argument("value", help="Value to store")

    # ── analytics ──
    an_top = subparsers.add_parser("analytics", help="Analytics queries")
    an_top.add_argument(
        "--db",
        dest="db_path",
        type=str,
        help="Path to SQLite DB file",
    )
    an_sub = an_top.add_subparsers(dest="an_cmd", help="Analytics subcommands")
    an_sub.add_parser("summary", help="Full analytics summary")

    an_qe = an_sub.add_parser("query-events", help="Query audit events")
    an_qe.add_argument("--type", type=str, help="Event type filter")
    an_qe.add_argument("--actor", type=str, help="Actor filter")
    an_qe.add_argument("--since", type=str, help="Start date (ISO)")
    an_qe.add_argument("--until", type=str, help="End date (ISO)")
    an_qe.add_argument("--limit", type=int, default=50, help="Max results")

    an_qs = an_sub.add_parser("query-snapshots", help="Query DAG snapshots")
    an_qs.add_argument("--name", type=str, help="Name pattern filter")
    an_qs.add_argument("--since", type=str, help="Start date (ISO)")
    an_qs.add_argument("--until", type=str, help="End date (ISO)")
    an_qs.add_argument("--limit", type=int, default=50, help="Max results")

    an_agg = an_sub.add_parser("aggregate", help="Aggregate events")
    an_agg.add_argument(
        "--group-by",
        dest="group_by",
        default="event_type",
        choices=["event_type", "actor"],
        help="Group by field",
    )
    an_agg.add_argument("--since", type=str, help="Start date (ISO)")
    an_agg.add_argument("--until", type=str, help="End date (ISO)")

    an_tl = an_sub.add_parser("timeline", help="Time-series event rollup")
    an_tl.add_argument(
        "--granularity",
        default="day",
        choices=["day", "hour"],
        help="Bucket granularity",
    )
    an_tl.add_argument("--since", type=str, help="Start date (ISO)")
    an_tl.add_argument("--until", type=str, help="End date (ISO)")

    an_search = an_sub.add_parser("search", help="Search event details")
    an_search.add_argument("query", help="Search term")

    # ── stream ──
    st_top = subparsers.add_parser("stream", help="Streaming result export")
    st_sub = st_top.add_subparsers(dest="st_cmd", help="Stream subcommands")

    st_ecl = st_sub.add_parser("ecl", help="Stream ECL results")
    st_ecl.add_argument("--data", type=str, help="Data file")
    st_ecl.add_argument(
        "--format",
        dest="fmt",
        default="ndjson",
        choices=["ndjson", "sse", "json"],
        help="Output format",
    )
    st_ecl.add_argument(
        "--page",
        type=int,
        default=0,
        help="Page number (0=stream all)",
    )
    st_ecl.add_argument(
        "--page-size",
        dest="page_size",
        type=int,
        default=50,
        help="Records per page",
    )

    st_snap = st_sub.add_parser("snapshots", help="Stream snapshot list")
    st_snap.add_argument(
        "--db",
        dest="db_path",
        type=str,
        help="Path to SQLite DB file",
    )
    st_snap.add_argument(
        "--format",
        dest="fmt",
        default="ndjson",
        choices=["ndjson", "sse", "json"],
        help="Output format",
    )

    # ── audit-report ──
    ar_top = subparsers.add_parser("audit-report", help="Audit trail reports and compliance")
    ar_sub = ar_top.add_subparsers(dest="ar_cmd", help="Audit report subcommands")

    ar_full = ar_sub.add_parser("full", help="Full audit trail report")
    ar_full.add_argument("--action", type=str, help="Filter by action")
    ar_full.add_argument("--actor", type=str, help="Filter by actor")
    ar_full.add_argument("--keyword", type=str, help="Search keyword")
    ar_full.add_argument("--limit", type=int, default=1000, help="Max entries")

    ar_comp = ar_sub.add_parser("compliance", help="Compliance-focused report")
    ar_comp.add_argument(
        "--framework",
        default="sr11-7",
        choices=["sr11-7", "ccar", "cecl"],
        help="Regulatory framework",
    )

    ar_sub.add_parser("verify", help="Verify audit chain integrity")

    ar_actor = ar_sub.add_parser("actor", help="Actor-specific report")
    ar_actor.add_argument("actor_name", help="Actor identifier")

    # ── bulk ──
    bulk_top = subparsers.add_parser("bulk", help="Bulk data processing with progress tracking")
    bulk_sub = bulk_top.add_subparsers(dest="bulk_cmd", help="Bulk subcommands")

    bulk_run = bulk_sub.add_parser("run", help="Run bulk processing")
    bulk_run.add_argument(
        "--input",
        type=str,
        required=True,
        help="Input file (JSON-lines)",
    )
    bulk_run.add_argument(
        "--chunk-size",
        dest="chunk_size",
        type=int,
        default=100,
        help="Records per chunk (default: 100)",
    )
    bulk_run.add_argument(
        "--max-errors",
        dest="max_errors",
        type=int,
        default=0,
        help="Stop after N errors (0=unlimited)",
    )
    bulk_run.add_argument(
        "--workers",
        type=int,
        default=0,
        help="Thread workers (0=single-threaded)",
    )

    bulk_dry = bulk_sub.add_parser("dry-run", help="Dry-run to estimate timing")
    bulk_dry.add_argument(
        "--input",
        type=str,
        required=True,
        help="Input file (JSON-lines)",
    )
    bulk_dry.add_argument(
        "--sample-size",
        dest="sample_size",
        type=int,
        default=10,
        help="Sample size for dry run (default: 10)",
    )

    # ── filing ──
    filing_top = subparsers.add_parser(
        "filing", help="Regulatory filing package generation (CECL/CCAR)"
    )
    filing_sub = filing_top.add_subparsers(dest="filing_cmd", help="Filing subcommands")
    fil_gen = filing_sub.add_parser("generate", help="Generate full regulatory filing package")
    fil_gen.add_argument(
        "--institution-id",
        dest="institution_id",
        default="BANK001",
        help="Institution identifier (default: BANK001)",
    )
    fil_gen.add_argument(
        "--period",
        default="2026Q1",
        help="Reporting period (default: 2026Q1)",
    )
    fil_gen.add_argument(
        "--output",
        dest="output_dir",
        default=".",
        help="Output directory (default: .)",
    )
    fil_gen.add_argument(
        "--seed",
        type=int,
        default=42,
        help="Random seed (default: 42)",
    )
    fil_gen.add_argument(
        "--n-samples",
        dest="n_samples",
        type=int,
        default=500,
        help="Synthetic portfolio size (default: 500)",
    )
    fil_gen.add_argument(
        "--horizon",
        dest="horizon_quarters",
        type=int,
        default=9,
        help="CCAR horizon in quarters (default: 9)",
    )
    fil_val = filing_sub.add_parser(
        "validate", help="Re-verify a generated filing package (SHA-256 + schema)"
    )
    fil_val.add_argument(
        "output_dir",
        nargs="?",
        default=".",
        help="Output directory to validate (default: .)",
    )

    # ── run ──
    run_p = subparsers.add_parser(
        "run",
        help="Run full CECL/CCAR filing pipeline end-to-end",
    )
    run_p.add_argument(
        "--institution",
        required=True,
        help="Institution identifier (e.g. BANK001)",
    )
    run_p.add_argument(
        "--period",
        default="2026Q1",
        help="Reporting period (default: 2026Q1)",
    )
    run_p.add_argument(
        "--output",
        default="./filing",
        help="Output directory (default: ./filing)",
    )
    run_p.add_argument(
        "--loans",
        type=int,
        default=200,
        help="Number of synthetic loans (default: 200)",
    )
    run_p.add_argument(
        "--seed",
        type=int,
        default=42,
        help="Random seed (default: 42)",
    )
    run_p.add_argument(
        "--no-fair-lending",
        dest="no_fair_lending",
        action="store_true",
        help="Skip fair lending analysis",
    )
    run_p.add_argument(
        "--input-file",
        dest="input_file",
        metavar="PATH",
        default=None,
        help="Path to bank loan tape CSV/JSON (FR Y-14M format; skips synthetic generation)",
    )

    # ── demo ──
    demo_top = subparsers.add_parser(
        "demo", help="Bank-facing regulatory demo (CECL/CCAR end-to-end)"
    )
    demo_sub = demo_top.add_subparsers(dest="demo_cmd", help="Demo scenarios")
    demo_reg = demo_sub.add_parser("regulatory", help="Full CECL/CCAR filing workflow")
    demo_reg.add_argument(
        "--output-dir",
        dest="output_dir",
        default="./demo_output",
        help="Output directory (default: ./demo_output)",
    )
    demo_reg.add_argument(
        "--institution-id",
        dest="institution_id",
        default="DEMO_BANK_001",
        help="Institution identifier (default: DEMO_BANK_001)",
    )
    demo_reg.add_argument(
        "--period",
        default="2025Q4",
        help="Reporting period (default: 2025Q4)",
    )
    demo_reg.add_argument(
        "--seed",
        type=int,
        default=42,
        help="Random seed for reproducibility (default: 42)",
    )

    return parser


# ── Tenant subcommand handlers ────────────────────────────────


def _cmd_tenant_create(args, _engine: OrxaqEngine) -> int:
    from orxaq.enterprise.tenant import TenantStore

    store = TenantStore()
    try:
        ctx = store.create(
            args.tenant_id,
            name=args.name or args.tenant_id,
            tier=args.tier,
            max_entities=args.max_entities,
            max_edges=args.max_edges,
        )
    except ValueError as e:
        print(f"Error: {e}")
        return 1
    if _is_json(args):
        return _json_out({"tenant_id": ctx.tenant_id, "name": ctx.name, "tier": ctx.tier})
    print(f"Created tenant: {ctx.tenant_id}")
    print(f"  Name:         {ctx.name}")
    print(f"  Tier:         {ctx.tier}")
    print(f"  Max entities: {ctx.max_entities}")
    print(f"  Max edges:    {ctx.max_edges}")
    return 0


def _cmd_tenant_list(args, _engine: OrxaqEngine) -> int:
    from orxaq.enterprise.tenant import TenantStore

    store = TenantStore()
    tenants = store.list_tenants()
    if _is_json(args):
        return _json_out(
            {
                "tenants": [
                    {"tenant_id": t.tenant_id, "name": t.name, "tier": t.tier} for t in tenants
                ]
            }
        )
    if not tenants:
        print("No tenants registered")
        return 0
    print(f"Tenants ({len(tenants)})")
    for t in tenants:
        print(f"  {t.tenant_id:<20} {t.name:<20} [{t.tier}]")
    return 0


def _cmd_tenant_get(args, _engine: OrxaqEngine) -> int:
    from orxaq.enterprise.tenant import TenantStore

    store = TenantStore()
    ctx = store.get(args.tenant_id)
    if ctx is None:
        print(f"Tenant not found: {args.tenant_id}")
        return 1
    if _is_json(args):
        return _json_out(
            {
                "tenant_id": ctx.tenant_id,
                "name": ctx.name,
                "tier": ctx.tier,
                "max_entities": ctx.max_entities,
                "max_edges": ctx.max_edges,
                "is_unlimited": ctx.is_unlimited,
            }
        )
    print(f"Tenant: {ctx.tenant_id}")
    print(f"  Name:         {ctx.name}")
    print(f"  Tier:         {ctx.tier}")
    print(f"  Max entities: {ctx.max_entities}")
    print(f"  Max edges:    {ctx.max_edges}")
    print(f"  Unlimited:    {ctx.is_unlimited}")
    return 0


def _cmd_tenant_delete(args, _engine: OrxaqEngine) -> int:
    from orxaq.enterprise.tenant import TenantStore

    store = TenantStore()
    if store.delete(args.tenant_id):
        print(f"Deleted: {args.tenant_id}")
        return 0
    print(f"Not found: {args.tenant_id}")
    return 1


def main(argv: list[str] | None = None) -> int:
    """CLI entry point."""
    parser = build_parser()
    args = parser.parse_args(argv)

    if not args.command:
        parser.print_help()
        return 1

    engine = OrxaqEngine()

    dispatch = {
        "status": _cmd_status,
        "discover": _cmd_discover,
        "validate": _cmd_validate,
        "audit": _cmd_audit,
        "serve": _cmd_serve,
        "edit": _cmd_edit,
        "simulate": _cmd_simulate,
        "run": _cmd_run,
    }

    if args.command in dispatch:
        return dispatch[args.command](args, engine)

    if args.command == "credit":
        credit_dispatch = {
            "ontology": _cmd_credit_ontology,
            "pd": _cmd_credit_pd,
            "ecl": _cmd_credit_ecl,
            "fairness": _cmd_credit_fairness,
            "portfolio": _cmd_credit_portfolio,
            "stress": _cmd_credit_stress,
            "report": _cmd_credit_report,
            "regulatory-export": _cmd_credit_regulatory_export,
            "export-regulatory": _cmd_credit_export_regulatory,
            "regulatory-report": _cmd_credit_regulatory_report,
            "compare": _cmd_credit_compare,
        }
        cmd = getattr(args, "credit_cmd", None)
        if cmd in credit_dispatch:
            return credit_dispatch[cmd](args, engine)
        parser.parse_args(["credit", "--help"])
        return 1

    if args.command == "fabric":
        fabric_dispatch = {
            "ingest": _cmd_fabric_ingest,
            "profile": _cmd_fabric_profile,
            "quality": _cmd_fabric_quality,
        }
        cmd = getattr(args, "fabric_cmd", None)
        if cmd in fabric_dispatch:
            return fabric_dispatch[cmd](args, engine)
        parser.parse_args(["fabric", "--help"])
        return 1

    if args.command == "providers":
        providers_dispatch = {
            "check": _cmd_providers_check,
            "list": _cmd_providers_list,
            "cost": _cmd_providers_cost,
            "cache": _cmd_providers_cache,
            "stats": _cmd_providers_stats,
        }
        cmd = getattr(args, "providers_cmd", None)
        if cmd in providers_dispatch:
            return providers_dispatch[cmd](args, engine)
        parser.parse_args(["providers", "--help"])
        return 1

    if args.command == "model":
        model_dispatch = {
            "save": _cmd_model_save,
            "load": _cmd_model_load,
            "list": _cmd_model_list,
            "versions": _cmd_model_versions,
            "diff": _cmd_model_diff,
            "regression": _cmd_model_regression,
            "metrics": _cmd_model_metrics,
        }
        cmd = getattr(args, "model_cmd", None)
        if cmd in model_dispatch:
            return model_dispatch[cmd](args, engine)
        parser.parse_args(["model", "--help"])
        return 1

    if args.command == "export":
        export_dispatch = {
            "dag": _cmd_export_dag,
            "ecl": _cmd_export_ecl,
            "edges": _cmd_export_edges,
            "nodes": _cmd_export_nodes,
        }
        cmd = getattr(args, "export_cmd", None)
        if cmd in export_dispatch:
            return export_dispatch[cmd](args, engine)
        parser.parse_args(["export", "--help"])
        return 1

    if args.command == "learn":
        learn_dispatch = {
            "summary": _cmd_learn_summary,
            "experiments": _cmd_learn_experiments,
            "best": _cmd_learn_best,
            "compare": _cmd_learn_compare,
        }
        cmd = getattr(args, "learn_cmd", None)
        if cmd in learn_dispatch:
            return learn_dispatch[cmd](args, engine)
        parser.parse_args(["learn", "--help"])
        return 1

    if args.command == "cost":
        cost_dispatch = {
            "status": _cmd_cost_status,
            "set": _cmd_cost_set,
            "summary": _cmd_cost_summary,
            "budget": _cmd_cost_budget,
            "pricing": _cmd_cost_pricing,
        }
        cmd = getattr(args, "cost_cmd", None)
        if cmd in cost_dispatch:
            return cost_dispatch[cmd](args, engine)
        parser.parse_args(["cost", "--help"])
        return 1

    if args.command == "tenant":
        tenant_dispatch = {
            "create": _cmd_tenant_create,
            "list": _cmd_tenant_list,
            "get": _cmd_tenant_get,
            "delete": _cmd_tenant_delete,
        }
        cmd = getattr(args, "tenant_cmd", None)
        if cmd in tenant_dispatch:
            return tenant_dispatch[cmd](args, engine)
        parser.parse_args(["tenant", "--help"])
        return 1

    if args.command == "batch":
        batch_dispatch = {
            "run": _cmd_batch_run,
            "list": _cmd_batch_list,
            "stats": _cmd_batch_stats,
            "clear": _cmd_batch_clear,
        }
        cmd = getattr(args, "batch_cmd", None)
        if cmd in batch_dispatch:
            return batch_dispatch[cmd](args, engine)
        parser.parse_args(["batch", "--help"])
        return 1

    if args.command == "metrics":
        metrics_dispatch = {
            "snapshot": _cmd_metrics_snapshot,
            "prometheus": _cmd_metrics_prometheus,
            "health": _cmd_metrics_health,
            "recent": _cmd_metrics_recent,
        }
        cmd = getattr(args, "metrics_cmd", None)
        if cmd in metrics_dispatch:
            return metrics_dispatch[cmd](args, engine)
        parser.parse_args(["metrics", "--help"])
        return 1

    if args.command == "db":
        db_dispatch = {
            "snapshots": _cmd_db_snapshots,
            "save-snapshot": _cmd_db_save_snapshot,
            "events": _cmd_db_events,
            "get": _cmd_db_get,
            "put": _cmd_db_put,
        }
        cmd = getattr(args, "db_cmd", None)
        if cmd in db_dispatch:
            return db_dispatch[cmd](args, engine)
        parser.parse_args(["db", "--help"])
        return 1

    if args.command == "analytics":
        an_dispatch = {
            "summary": _cmd_analytics_summary,
            "query-events": _cmd_analytics_query_events,
            "query-snapshots": _cmd_analytics_query_snapshots,
            "aggregate": _cmd_analytics_aggregate,
            "timeline": _cmd_analytics_timeline,
            "search": _cmd_analytics_search,
        }
        cmd = getattr(args, "an_cmd", None)
        if cmd in an_dispatch:
            return an_dispatch[cmd](args, engine)
        parser.parse_args(["analytics", "--help"])
        return 1

    if args.command == "filing":
        filing_dispatch = {
            "generate": _cmd_filing_generate,
            "validate": _cmd_filing_validate,
        }
        cmd = getattr(args, "filing_cmd", None)
        if cmd in filing_dispatch:
            return filing_dispatch[cmd](args, engine)
        parser.parse_args(["filing", "--help"])
        return 1

    if args.command == "stream":
        st_dispatch = {
            "ecl": _cmd_stream_ecl,
            "snapshots": _cmd_stream_snapshots,
        }
        cmd = getattr(args, "st_cmd", None)
        if cmd in st_dispatch:
            return st_dispatch[cmd](args, engine)
        parser.parse_args(["stream", "--help"])
        return 1

    if args.command == "audit-report":
        ar_dispatch = {
            "full": _cmd_audit_report_full,
            "compliance": _cmd_audit_report_compliance,
            "verify": _cmd_audit_report_verify,
            "actor": _cmd_audit_report_actor,
        }
        cmd = getattr(args, "ar_cmd", None)
        if cmd in ar_dispatch:
            return int(ar_dispatch[cmd](args, engine))
        parser.parse_args(["audit-report", "--help"])
        return 1

    if args.command == "bulk":
        bulk_dispatch = {
            "run": _cmd_bulk_run,
            "dry-run": _cmd_bulk_dry_run,
        }
        cmd = getattr(args, "bulk_cmd", None)
        if cmd in bulk_dispatch:
            return int(bulk_dispatch[cmd](args, engine))
        parser.parse_args(["bulk", "--help"])
        return 1

    if args.command == "demo":
        return _cmd_demo(args, engine)

    parser.print_help()
    return 1


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