"""
Salesforce metadata pack orchestration (plan/apply/drift/verify/doctor).
"""

from __future__ import annotations

import hashlib
import json
import shutil
import tempfile
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Tuple

from g_gremlin import sfdc as sfdc_helpers
from g_gremlin.errors import missing_pyyaml_error
from g_gremlin.provenance import build_provenance, write_sidecar
from g_gremlin.sfdc_metadata_components import ComponentInventory, MetadataComponent, MetadataType, scan_source_dir
from g_gremlin.sfdc_metadata_destructive import (
    SfdcDestructiveManifest,
    load_destructive_manifest,
    validate_destructive_coverage,
)
from g_gremlin.sfdc_metadata_manifest import (
    generate_destructive_changes_xml,
    write_package_xml,
)
from g_gremlin.sfdc_metadata_retrieve import retrieve_components
from g_gremlin.sfdc_metadata_smoke import SmokeReport, load_smoke_assertions, run_smoke


@dataclass
class MetadataPackPlan:
    payload: Dict[str, Any]

    def to_dict(self) -> Dict[str, Any]:
        return self.payload


@dataclass
class MetadataApplyResult:
    payload: Dict[str, Any]

    def to_dict(self) -> Dict[str, Any]:
        return self.payload


@dataclass
class MetadataDriftResult:
    payload: Dict[str, Any]

    def to_dict(self) -> Dict[str, Any]:
        return self.payload


def init_pack(dir_path: Path) -> None:
    dir_path.mkdir(parents=True, exist_ok=True)
    (dir_path / "artifacts").mkdir(exist_ok=True)
    source_root = dir_path / "force-app" / "main" / "default"
    source_root.mkdir(parents=True, exist_ok=True)
    (source_root / "objects").mkdir(exist_ok=True)
    (source_root / "permissionsets").mkdir(exist_ok=True)
    (source_root / "queues").mkdir(exist_ok=True)
    (source_root / "flows").mkdir(exist_ok=True)
    (source_root / "flexipages").mkdir(exist_ok=True)
    (source_root / "classes").mkdir(exist_ok=True)
    (source_root / "triggers").mkdir(exist_ok=True)
    (source_root / "lwc").mkdir(exist_ok=True)

    pack_yaml = dir_path / "pack.yaml"
    if not pack_yaml.exists():
        pack_yaml.write_text(
            "\n".join(
                [
                    "name: sfdc_metadata_pack",
                    "pack_type: metadata",
                    "api_version: '62.0'",
                    "",
                    "include:",
                    "  custom_fields: true",
                    "  permission_sets: true",
                    "  validation_rules: true",
                    "  queues: true",
                    "  flows: false",
                    "  flexipages: false",
                    "  layouts: false",
                    "  apex_classes: false",
                    "  apex_triggers: false",
                    "  lwc: false",
                    "  record_types: false",
                    "",
                    "options:",
                    "  allow_delete_fields: false",
                    "  allow_delete_rules: false",
                    "  allow_delete_permsets: false",
                    "  allow_delete_record_types: false",
                    "  check_only_first: true",
                    "  smoke_after_apply: true",
                    "  max_plan_age_minutes: null",
                    "",
                    "source_dir: force-app",
                    "target_org: null",
                    "",
                    "smoke: []",
                    "",
                ]
            ),
            encoding="utf-8",
        )

    destructive_yaml = dir_path / "destructive.yaml"
    if not destructive_yaml.exists():
        destructive_yaml.write_text(
            "\n".join(
                [
                    "# Destructive operations manifest for this pack.",
                    "# Each operation requires BOTH a manifest entry AND the corresponding allow_* flag.",
                    "",
                    "remove_fields:",
                    "  # - object: Account",
                    "  #   field: Old_Scout_Flag__c",
                    "  #   reason: 'Replaced by Scout_Flagged__c'",
                    "",
                    "remove_validation_rules:",
                    "  # - object: Opportunity",
                    "  #   rule: Old_Require_Notes",
                    "  #   reason: 'Superseded by Scout_Require_Notes_On_Close'",
                    "",
                    "remove_permission_sets:",
                    "  # - name: Old_Scout_PermSet",
                    "  #   reason: 'Consolidated into Scout_Hygiene_Agent'",
                    "",
                    "remove_record_types:",
                    "  # - object: Account",
                    "  #   record_type: Legacy_B2C",
                    "  #   reason: 'Retired in favor of Unified_Account'",
                    "",
                ]
            ),
            encoding="utf-8",
        )

    readme = dir_path / "README.md"
    if not readme.exists():
        readme.write_text(
            "\n".join(
                [
                    "# Salesforce Metadata Pack",
                    "",
                    "Workflow:",
                    "1) g-gremlin sfdc metadata-pack plan .",
                    "2) g-gremlin sfdc metadata-pack apply . --yes",
                    "3) g-gremlin sfdc metadata-pack verify .",
                    "4) g-gremlin sfdc metadata-pack drift .",
                    "",
                ]
            ),
            encoding="utf-8",
        )


def load_pack_yaml(path: Path) -> Dict[str, Any]:
    if not path.exists():
        raise ValueError(f"Pack file not found: {path}")
    raw_text = path.read_text(encoding="utf-8")
    if path.suffix.lower() in {".yaml", ".yml"}:
        try:
            import yaml  # type: ignore
        except Exception as exc:
            raise missing_pyyaml_error() from exc
        return yaml.safe_load(raw_text) or {}
    return json.loads(raw_text)


def plan_pack(pack_dir: Path, *, org_alias: Optional[str] = None) -> MetadataPackPlan:
    org_alias = org_alias or sfdc_helpers.get_configured_org()
    pack = load_pack_yaml(pack_dir / "pack.yaml")
    pack_name = pack.get("name") or pack_dir.name
    api_version = str(pack.get("api_version") or "59.0")
    source_dir = str(pack.get("source_dir") or "force-app")
    options = dict(pack.get("options") or {})

    include_types = _resolve_include(pack.get("include") or {})

    local_source_root = _resolve_source_root(pack_dir, source_dir)
    local_inventory = scan_source_dir(local_source_root, include=include_types)

    destructive_manifest = load_destructive_manifest(pack_dir)
    destructive_components = _components_from_destructive(destructive_manifest)
    destructive_errors = validate_destructive_coverage(destructive_manifest, options)
    if destructive_errors:
        raise ValueError("Destructive manifest validation failed: " + "; ".join(destructive_errors))
    effective_include = include_types | {comp.type for comp in destructive_components}

    remote_snapshot = _snapshot_remote(
        pack_dir=pack_dir,
        source_dir=source_dir,
        api_version=api_version,
        components=_merge_components(local_inventory.components, destructive_components),
        include_types=effective_include,
        org_alias=org_alias,
    )

    local_entries = _build_manifest_entries(local_source_root, local_inventory)
    remote_entries = remote_snapshot.entries

    compare = _compare_entries(remote_entries, local_entries, pack_name=pack_name)

    toolchain = _toolchain_fingerprint()
    auth = _auth_fingerprint(org_alias)

    payload = {
        "$schema": "SfdcMetadataPackPlan/v1",
        "plan_id": _new_plan_id(),
        "created_at": datetime.now(timezone.utc).isoformat(),
        "pack_name": pack_name,
        "source_dir": source_dir,
        "api_version": api_version,
        "summary": compare["summary"],
        "by_type": compare["by_type"],
        "details": compare["details"],
        "baseline_fingerprint": remote_snapshot.fingerprint,
        "toolchain_fingerprint": toolchain,
        "auth_fingerprint": auth,
        "include": pack.get("include") or {},
        "options": options,
        "destructive": _destructive_summary(destructive_manifest),
        "validation": {
            "blocked": bool(destructive_errors),
            "errors": destructive_errors,
            "warnings": [],
        },
        "retrieval": remote_snapshot.retrieve_result,
    }
    payload["plan_hash"] = _compute_plan_hash(payload)

    artifact_dir = _artifact_dir(pack_dir)
    artifact_dir.mkdir(parents=True, exist_ok=True)
    plan_path = artifact_dir / "plan.json"
    _write_json(plan_path, payload)

    return MetadataPackPlan(payload=payload)


def apply_pack(
    pack_dir: Path,
    *,
    org_alias: Optional[str] = None,
    plan_path: Optional[Path] = None,
    yes: bool = False,
    replan: bool = False,
    check_only: bool = False,
    expected_plan_hash: str | None = None,
) -> MetadataApplyResult:
    org_alias = org_alias or sfdc_helpers.get_configured_org()
    if not yes:
        raise ValueError("Refusing to apply without --yes.")

    pack = load_pack_yaml(pack_dir / "pack.yaml")
    api_version = str(pack.get("api_version") or "59.0")
    source_dir = str(pack.get("source_dir") or "force-app")
    options = dict(pack.get("options") or {})

    if not plan_path:
        plan_path = _latest_plan(pack_dir)
    if not plan_path or not plan_path.exists():
        raise ValueError("No plan found. Run: g-gremlin sfdc metadata-pack plan <dir>")

    plan = json.loads(plan_path.read_text(encoding="utf-8"))
    stored_hash = str(plan.get("plan_hash") or "").strip()
    if stored_hash:
        recomputed = _compute_plan_hash(plan)
        if recomputed != stored_hash:
            raise ValueError("Plan hash mismatch -- plan file may have been tampered with.")
    if expected_plan_hash:
        expected = str(expected_plan_hash).strip()
        if not stored_hash:
            raise ValueError("Plan hash missing from plan file; cannot verify --plan-hash.")
        if stored_hash != expected:
            raise ValueError("Provided --plan-hash does not match plan file hash.")
    _validate_auth_fingerprint(plan, org_alias=org_alias)

    validation = plan.get("validation") or {}
    if validation.get("blocked"):
        raise ValueError("Plan is blocked due to validation errors. Re-run plan after fixing errors.")

    max_plan_age = (plan.get("options") or {}).get("max_plan_age_minutes")
    if max_plan_age:
        created_at = plan.get("created_at")
        if created_at:
            created_dt = datetime.fromisoformat(created_at)
            age_minutes = (datetime.now(timezone.utc) - created_dt).total_seconds() / 60.0
            if age_minutes > float(max_plan_age):
                raise ValueError(
                    f"Plan is {age_minutes:.1f} minutes old; exceeds max_plan_age_minutes={max_plan_age}."
                )

    destructive_manifest = load_destructive_manifest(pack_dir)
    destructive_components = _components_from_destructive(destructive_manifest)

    include_types = _resolve_include(pack.get("include") or {})
    effective_include = include_types | {comp.type for comp in destructive_components}

    artifact_dir = _artifact_dir(pack_dir)
    artifact_dir.mkdir(parents=True, exist_ok=True)
    before_dir = artifact_dir / "before_snapshot"

    # Snapshot remote before deploy to validate baseline.
    before_snapshot = _snapshot_remote(
        pack_dir=pack_dir,
        source_dir=source_dir,
        api_version=api_version,
        components=_components_from_plan(plan, destructive_components),
        include_types=effective_include,
        org_alias=org_alias,
        materialize_dir=before_dir,
    )

    baseline_fp = before_snapshot.fingerprint
    if baseline_fp != plan.get("baseline_fingerprint"):
        if not replan:
            raise ValueError("Baseline fingerprint mismatch. Re-run plan or use --replan.")
        replanned = plan_pack(pack_dir, org_alias=org_alias)
        plan = replanned.payload
        before_snapshot = _snapshot_remote(
            pack_dir=pack_dir,
            source_dir=source_dir,
            api_version=api_version,
            components=_components_from_plan(plan, destructive_components),
            include_types=effective_include,
            org_alias=org_alias,
            materialize_dir=before_dir,
        )
        if before_snapshot.fingerprint != plan.get("baseline_fingerprint"):
            raise ValueError("Baseline fingerprint mismatch after replan.")

    source_root = _resolve_source_root(pack_dir, source_dir)
    if not source_root.exists():
        raise ValueError(f"source_dir not found: {source_root}")

    _write_json(artifact_dir / "plan.json", plan)

    deploy_log: Dict[str, Any] = {"check_only": None, "deploy": None, "destructive": None}

    check_only_first = bool(options.get("check_only_first", False))
    if check_only or check_only_first:
        check_result = sfdc_helpers.project_deploy_start(
            source_root,
            org_alias=org_alias,
            dry_run=True,
            wait=10,
        )
        deploy_log["check_only"] = check_result
        if _deploy_failed(check_result):
            _write_json(artifact_dir / "deploy_log.json", deploy_log)
            raise ValueError("Validation deploy failed. Review deploy_log.json for details.")
        if check_only:
            payload = {
                "$schema": "SfdcMetadataApply/v1",
                "status": "check_only",
                "deploy_id": _extract_deploy_id(check_result),
                "plan_id": plan.get("plan_id"),
                "summary": {"deployed": 0, "failed": 0, "total": 0},
                "component_outcomes": [],
                "artifact_dir": str(artifact_dir),
                "warnings": [],
                "fingerprints": {
                    "baseline": baseline_fp,
                    "toolchain": plan.get("toolchain_fingerprint"),
                    "auth": plan.get("auth_fingerprint"),
                },
            }
            apply_path = artifact_dir / "apply.json"
            _write_json(apply_path, payload)
            _write_json(artifact_dir / "deploy_log.json", deploy_log)
            return MetadataApplyResult(payload=payload)

    deploy_result = sfdc_helpers.project_deploy_start(
        source_root,
        org_alias=org_alias,
        dry_run=False,
        wait=10,
    )
    deploy_log["deploy"] = deploy_result

    report_payload = _deploy_report_payload(deploy_result, org_alias)
    component_outcomes = _normalize_component_outcomes(report_payload)

    destructive_outcomes: List[Dict[str, Any]] = []
    if destructive_components:
        destructive_result = _deploy_destructive(destructive_components, api_version, org_alias=org_alias)
        deploy_log["destructive"] = destructive_result
        destructive_report = _deploy_report_payload(destructive_result, org_alias)
        destructive_outcomes = _normalize_component_outcomes(destructive_report)

    component_outcomes.extend(destructive_outcomes)

    after_dir = artifact_dir / "after_snapshot"
    after_snapshot = _snapshot_remote(
        pack_dir=pack_dir,
        source_dir=source_dir,
        api_version=api_version,
        components=_components_from_plan(plan, destructive_components),
        include_types=effective_include,
        org_alias=org_alias,
        materialize_dir=after_dir,
    )

    compare = _compare_entries(
        before_snapshot.entries,
        after_snapshot.entries,
        pack_name=plan.get("pack_name") or pack.get("name") or pack_dir.name,
    )
    _write_json(artifact_dir / "compare.json", compare)

    summary_counts = _component_outcome_summary(component_outcomes)
    payload = {
        "$schema": "SfdcMetadataApply/v1",
        "status": _apply_status(summary_counts),
        "deploy_id": _extract_deploy_id(deploy_result),
        "plan_id": plan.get("plan_id"),
        "summary": summary_counts,
        "component_outcomes": component_outcomes,
        "artifact_dir": str(artifact_dir),
        "warnings": [],
        "fingerprints": {
            "baseline": baseline_fp,
            "toolchain": plan.get("toolchain_fingerprint"),
            "auth": plan.get("auth_fingerprint"),
        },
    }

    apply_path = artifact_dir / "apply.json"
    _write_json(apply_path, payload)
    _write_json(artifact_dir / "deploy_log.json", deploy_log)

    summary_text = _format_summary(compare)
    (artifact_dir / "SUMMARY.md").write_text(summary_text + "\n", encoding="utf-8")

    prov = build_provenance(
        command="sfdc.metadata_pack.apply",
        args={
            "pack_dir": str(pack_dir),
            "org_alias": org_alias,
            "check_only": check_only,
            "replan": replan,
        },
        inputs=[pack_dir / "pack.yaml", plan_path],
        provider="sfdc",
    )
    _write_json(artifact_dir / "provenance.json", prov)
    try:
        write_sidecar(apply_path, prov, provenance_dir=None)
    except Exception:
        pass

    if bool(options.get("smoke_after_apply")):
        report = verify_pack(pack_dir, org_alias=org_alias)
        _write_json(artifact_dir / "smoke.json", report.to_dict())

    return MetadataApplyResult(payload=payload)


def compute_drift(pack_dir: Path, *, org_alias: Optional[str] = None) -> MetadataDriftResult:
    pack = load_pack_yaml(pack_dir / "pack.yaml")
    api_version = str(pack.get("api_version") or "59.0")
    source_dir = str(pack.get("source_dir") or "force-app")
    include_types = _resolve_include(pack.get("include") or {})

    local_source_root = _resolve_source_root(pack_dir, source_dir)
    local_inventory = scan_source_dir(local_source_root, include=include_types)
    destructive_manifest = load_destructive_manifest(pack_dir)
    destructive_components = _components_from_destructive(destructive_manifest)
    effective_include = include_types | {comp.type for comp in destructive_components}

    remote_snapshot = _snapshot_remote(
        pack_dir=pack_dir,
        source_dir=source_dir,
        api_version=api_version,
        components=_merge_components(local_inventory.components, destructive_components),
        include_types=effective_include,
        org_alias=org_alias,
    )

    local_entries = _build_manifest_entries(local_source_root, local_inventory)
    compare = _compare_entries(
        remote_snapshot.entries,
        local_entries,
        pack_name=pack.get("name") or pack_dir.name,
    )
    payload = {
        "$schema": "SfdcMetadataDrift/v1",
        "has_drift": compare["summary"]["added"]
        + compare["summary"]["changed"]
        + compare["summary"]["removed"]
        > 0,
        "summary": compare["summary"],
        "by_type": compare["by_type"],
        "details": compare["details"],
    }
    return MetadataDriftResult(payload=payload)


def verify_pack(pack_dir: Path, *, org_alias: Optional[str] = None) -> SmokeReport:
    pack = load_pack_yaml(pack_dir / "pack.yaml")
    assertions = load_smoke_assertions(pack)
    return run_smoke(assertions, org_alias=org_alias)


def pack_doctor(pack_dir: Path) -> Dict[str, Any]:
    missing: List[str] = []
    notes: List[str] = []

    if not (pack_dir / "pack.yaml").exists():
        missing.append("pack.yaml")
    source_dir = None
    if (pack_dir / "pack.yaml").exists():
        try:
            pack = load_pack_yaml(pack_dir / "pack.yaml")
            source_dir = str(pack.get("source_dir") or "force-app")
        except Exception as exc:
            notes.append(f"pack.yaml parse error: {exc}")
    if source_dir:
        source_root = _resolve_source_root(pack_dir, source_dir)
        if not source_root.exists():
            missing.append(source_dir)

    try:
        sfdc_helpers._find_sf_cli()
        sfdc_helpers.get_cli_version()
    except Exception as exc:
        notes.append(f"sf cli unavailable: {exc}")

    ok = not missing
    return {"ok": ok, "missing_files": missing, "notes": notes}


# ----------------- helpers -----------------


@dataclass
class _Snapshot:
    entries: List[Dict[str, Any]]
    fingerprint: str
    retrieve_result: Dict[str, Any]


def _snapshot_remote(
    *,
    pack_dir: Path,
    source_dir: str,
    api_version: str,
    components: Iterable[MetadataComponent],
    include_types: Iterable[MetadataType],
    org_alias: Optional[str],
    materialize_dir: Path | None = None,
) -> _Snapshot:
    with tempfile.TemporaryDirectory() as tmpdir:
        out_dir = Path(tmpdir) / "remote"
        retrieve_result = retrieve_components(
            _dedupe_components(list(components)),
            out_dir,
            org_alias=org_alias,
            api_version=api_version,
        )
        source_root = _resolve_source_root(out_dir, source_dir)
        if not source_root.exists():
            source_root = out_dir
        inventory = scan_source_dir(source_root, include=include_types)
        entries = _build_manifest_entries(source_root, inventory)
        manifest = _manifest_payload(entries)
        manifest_path = (Path(tmpdir) / "manifest.json")
        _write_json(manifest_path, manifest)
        fingerprint = _sha256_file(manifest_path)

        if materialize_dir:
            _copy_tree(source_root, materialize_dir / source_dir)
            _write_json(materialize_dir / "manifest.json", manifest)
        return _Snapshot(entries=entries, fingerprint=fingerprint, retrieve_result=retrieve_result)


def _copy_tree(source_root: Path, dest: Path) -> None:
    if dest.exists():
        shutil.rmtree(dest)
    dest.parent.mkdir(parents=True, exist_ok=True)
    if source_root.exists():
        shutil.copytree(source_root, dest)


def _merge_components(
    local_components: Iterable[MetadataComponent],
    destructive_components: Iterable[MetadataComponent],
) -> List[MetadataComponent]:
    return _dedupe_components(list(local_components) + list(destructive_components))


def _dedupe_components(components: List[MetadataComponent]) -> List[MetadataComponent]:
    seen: set[Tuple[str, str]] = set()
    unique: List[MetadataComponent] = []
    for comp in components:
        key = (comp.type.value, comp.api_name)
        if key in seen:
            continue
        seen.add(key)
        unique.append(comp)
    return unique


def _resolve_source_root(base_dir: Path, source_dir: str) -> Path:
    candidate = base_dir / source_dir
    if candidate.exists():
        return candidate
    fallback = base_dir / "force-app"
    if fallback.exists():
        return fallback
    return base_dir


def _resolve_include(include_cfg: Dict[str, Any]) -> set[MetadataType]:
    legacy_mapping = {
        "custom_fields": MetadataType.CUSTOM_FIELD,
        "permission_sets": MetadataType.PERMISSION_SET,
        "validation_rules": MetadataType.VALIDATION_RULE,
        "queues": MetadataType.QUEUE,
    }
    new_mapping = {
        "flows": MetadataType.FLOW,
        "flexipages": MetadataType.FLEXI_PAGE,
        "layouts": MetadataType.LAYOUT,
        "apex_classes": MetadataType.APEX_CLASS,
        "apex_triggers": MetadataType.APEX_TRIGGER,
        "lwc": MetadataType.LWC_BUNDLE,
        "record_types": MetadataType.RECORD_TYPE,
    }
    include_set: set[MetadataType] = set()
    if not include_cfg:
        return set(legacy_mapping.values())
    for key, mtype in legacy_mapping.items():
        if key in include_cfg:
            if include_cfg.get(key):
                include_set.add(mtype)
        else:
            include_set.add(mtype)
    for key, mtype in new_mapping.items():
        if include_cfg.get(key):
            include_set.add(mtype)
    return include_set


def _components_from_destructive(manifest: Optional[SfdcDestructiveManifest]) -> List[MetadataComponent]:
    components: List[MetadataComponent] = []
    if not manifest:
        return components
    for item in manifest.remove_fields:
        obj = str(item.get("object") or "").strip()
        field = str(item.get("field") or "").strip()
        if obj and field:
            components.append(
                MetadataComponent(
                    type=MetadataType.CUSTOM_FIELD,
                    api_name=f"{obj}.{field}",
                )
            )
    for item in manifest.remove_validation_rules:
        obj = str(item.get("object") or "").strip()
        rule = str(item.get("rule") or "").strip()
        if obj and rule:
            components.append(
                MetadataComponent(
                    type=MetadataType.VALIDATION_RULE,
                    api_name=f"{obj}.{rule}",
                )
            )
    for item in manifest.remove_permission_sets:
        name = str(item.get("name") or "").strip()
        if name:
            components.append(
                MetadataComponent(
                    type=MetadataType.PERMISSION_SET,
                    api_name=name,
                )
            )
    for item in manifest.remove_record_types:
        obj = str(item.get("object") or "").strip()
        record_type = str(item.get("record_type") or item.get("name") or "").strip()
        if obj and record_type:
            components.append(
                MetadataComponent(
                    type=MetadataType.RECORD_TYPE,
                    api_name=f"{obj}.{record_type}",
                )
            )
    return components


def _components_from_plan(plan: Dict[str, Any], destructive: List[MetadataComponent]) -> List[MetadataComponent]:
    details = plan.get("details") or []
    components: List[MetadataComponent] = []
    for item in details:
        type_name = str(item.get("type") or "")
        api_name = str(item.get("api_name") or "")
        if not type_name or not api_name:
            continue
        try:
            mtype = MetadataType(type_name)
        except Exception:
            continue
        components.append(MetadataComponent(type=mtype, api_name=api_name))
    components.extend(destructive)
    return components


def _sha256_directory(dir_path: Path) -> str:
    """Compute a composite SHA-256 over all files in a directory (for LWC bundles)."""
    h = hashlib.sha256()
    for fp in sorted(dir_path.rglob("*")):
        if fp.is_file():
            h.update(fp.relative_to(dir_path).as_posix().encode("utf-8"))
            h.update(_sha256_normalized(fp).encode("utf-8"))
    return h.hexdigest()


def _build_manifest_entries(source_root: Path, inventory: ComponentInventory) -> List[Dict[str, Any]]:
    entries: List[Dict[str, Any]] = []
    for comp in inventory.components:
        if comp.source_path is None:
            continue
        try:
            relpath = comp.source_path.relative_to(source_root).as_posix()
        except Exception:
            relpath = comp.source_path.as_posix()
        if comp.source_path.is_dir():
            sha256 = _sha256_directory(comp.source_path)
            size = sum(f.stat().st_size for f in comp.source_path.rglob("*") if f.is_file())
        else:
            sha256 = _sha256_normalized(comp.source_path)
            size = comp.source_path.stat().st_size if comp.source_path.exists() else 0
        entries.append(
            {
                "relpath": relpath,
                "sha256": sha256,
                "size": size,
                "type_tag": comp.type.value,
                "api_name": comp.api_name,
            }
        )
    entries.sort(key=lambda e: e["relpath"])
    return entries


def _manifest_payload(entries: List[Dict[str, Any]]) -> Dict[str, Any]:
    return {
        "version": 1,
        "total_files": len(entries),
        "entries": entries,
    }


def _compare_entries(
    remote_entries: List[Dict[str, Any]],
    local_entries: List[Dict[str, Any]],
    *,
    pack_name: str | None = None,
) -> Dict[str, Any]:
    remote_map = _entry_map(remote_entries)
    local_map = _entry_map(local_entries)
    keys = sorted(set(remote_map.keys()) | set(local_map.keys()))

    details: List[Dict[str, Any]] = []
    summary = {"added": 0, "changed": 0, "removed": 0, "unchanged": 0, "total": 0}
    by_type: Dict[str, Dict[str, int]] = {}

    for key in keys:
        r_entry = remote_map.get(key)
        l_entry = local_map.get(key)
        if l_entry and r_entry:
            status = "unchanged" if l_entry["sha256"] == r_entry["sha256"] else "changed"
        elif l_entry:
            status = "added"
        else:
            status = "removed"

        type_name = key[0]
        by_type.setdefault(type_name, {"added": 0, "changed": 0, "removed": 0, "unchanged": 0})
        by_type[type_name][status] += 1
        summary[status] += 1
        summary["total"] += 1

        details.append(
            {
                "type": type_name,
                "api_name": key[1],
                "status": status,
                "local_hash": (l_entry or {}).get("sha256"),
                "remote_hash": (r_entry or {}).get("sha256"),
            }
        )

    payload = {
        "schema": "SfdcMetadataCompare/v1",
        "summary": summary,
        "by_type": by_type,
        "details": details,
    }
    if pack_name:
        payload["pack_name"] = pack_name
    return payload


def _entry_map(entries: List[Dict[str, Any]]) -> Dict[Tuple[str, str], Dict[str, Any]]:
    mapping: Dict[Tuple[str, str], Dict[str, Any]] = {}
    for entry in entries:
        type_tag = str(entry.get("type_tag") or "")
        api_name = str(entry.get("api_name") or "")
        if type_tag and api_name:
            mapping[(type_tag, api_name)] = entry
    return mapping


def _artifact_dir(pack_dir: Path) -> Path:
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
    return pack_dir / "artifacts" / timestamp


def _latest_plan(pack_dir: Path) -> Optional[Path]:
    artifacts = pack_dir / "artifacts"
    if not artifacts.exists():
        return None
    dirs = sorted([d for d in artifacts.iterdir() if d.is_dir()], reverse=True)
    for d in dirs:
        plan_path = d / "plan.json"
        if plan_path.exists():
            return plan_path
    return None


def _toolchain_fingerprint() -> str:
    return sfdc_helpers.get_cli_version()


def _auth_fingerprint(org_alias: Optional[str]) -> Dict[str, Any]:
    return sfdc_helpers.get_org_identity(org_alias=org_alias)


def _validate_auth_fingerprint(plan_payload: Dict[str, Any], *, org_alias: Optional[str]) -> None:
    planned = plan_payload.get("auth_fingerprint") or {}
    planned_org_id = str(planned.get("org_id") or "").strip().upper()
    planned_instance = str(planned.get("instance_url") or "").strip().rstrip("/").lower()
    if not planned_org_id and not planned_instance:
        return

    current = sfdc_helpers.get_org_identity(org_alias=org_alias)
    current_org_id = str(current.get("org_id") or "").strip().upper()
    current_instance = str(current.get("instance_url") or "").strip().rstrip("/").lower()

    mismatch_detail = ""
    if planned_org_id and current_org_id and planned_org_id != current_org_id:
        mismatch_detail = f"plan org_id={planned_org_id}, current org_id={current_org_id}"
    elif not (planned_org_id and current_org_id) and planned_instance and current_instance and planned_instance != current_instance:
        mismatch_detail = (
            f"plan instance_url={planned_instance}, current instance_url={current_instance}"
        )

    if mismatch_detail:
        planned_alias = str(planned.get("alias") or "").strip() or "unknown"
        current_alias = str(current.get("alias") or "").strip() or "unknown"
        raise ValueError(
            "Plan auth fingerprint does not match target org. "
            f"{mismatch_detail}; plan alias={planned_alias}, current alias={current_alias}. "
            "Re-run plan against the intended org."
        )


def _new_plan_id() -> str:
    return str(uuid.uuid4())


def _sha256_file(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()


def _compute_plan_hash(plan_payload: Dict[str, Any]) -> str:
    canonical = {
        "plan_id": plan_payload.get("plan_id"),
        "details": plan_payload.get("details") or [],
        "destructive": plan_payload.get("destructive") or {},
        "baseline_fingerprint": plan_payload.get("baseline_fingerprint"),
    }
    raw = json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode("utf-8")
    return hashlib.sha256(raw).hexdigest()


def _sha256_normalized(path: Path) -> str:
    try:
        text = path.read_text(encoding="utf-8")
    except Exception:
        text = path.read_text(encoding="utf-8", errors="ignore")
    normalized = _normalize_text(text)
    return hashlib.sha256(normalized.encode("utf-8")).hexdigest()


def _normalize_text(text: str) -> str:
    text = text.replace("\r\n", "\n").replace("\r", "\n")
    lines = [line.rstrip() for line in text.split("\n")]
    normalized = "\n".join(lines)
    if normalized and not normalized.endswith("\n"):
        normalized += "\n"
    return normalized


def _write_json(path: Path, payload: Any) -> None:
    text = json.dumps(payload, sort_keys=True, indent=2)
    if not text.endswith("\n"):
        text += "\n"
    path.write_text(text, encoding="utf-8")


def _deploy_failed(payload: Dict[str, Any]) -> bool:
    result = payload.get("result") or {}
    if "success" in result:
        return not bool(result.get("success"))
    return False


def _extract_deploy_id(payload: Dict[str, Any]) -> str | None:
    result = payload.get("result") or {}
    for key in ("id", "jobId", "job_id", "deployId", "deploy_id"):
        value = result.get(key)
        if value:
            return str(value)
    return None


def _deploy_report_payload(payload: Dict[str, Any], org_alias: Optional[str]) -> Dict[str, Any]:
    deploy_id = _extract_deploy_id(payload)
    if not deploy_id:
        return payload
    try:
        return sfdc_helpers.project_deploy_report(deploy_id, org_alias=org_alias)
    except Exception:
        return payload


def _normalize_component_outcomes(report_payload: Dict[str, Any]) -> List[Dict[str, Any]]:
    result = report_payload.get("result") or {}
    details = result.get("details") or {}
    successes = details.get("componentSuccesses") or []
    failures = details.get("componentFailures") or []

    normalized: List[Dict[str, Any]] = []
    for item in successes:
        normalized.append(
            {
                "type": item.get("componentType") or item.get("type") or "",
                "api_name": item.get("fullName") or item.get("fullNameOrId") or item.get("name") or "",
                "status": "deployed",
            }
        )
    for item in failures:
        normalized.append(
            {
                "type": item.get("componentType") or item.get("type") or "",
                "api_name": item.get("fullName") or item.get("fullNameOrId") or item.get("name") or "",
                "status": "error",
                "message": item.get("problem") or item.get("error") or item.get("message") or "",
            }
        )
    return normalized


def _component_outcome_summary(outcomes: List[Dict[str, Any]]) -> Dict[str, int]:
    deployed = len([o for o in outcomes if o.get("status") == "deployed"])
    failed = len([o for o in outcomes if o.get("status") == "error"])
    return {"deployed": deployed, "failed": failed, "total": len(outcomes)}


def _apply_status(summary: Dict[str, int]) -> str:
    if summary.get("failed"):
        if summary.get("deployed"):
            return "partial"
        return "failed"
    return "success"


def _deploy_destructive(
    components: List[MetadataComponent],
    api_version: str,
    *,
    org_alias: Optional[str],
) -> Dict[str, Any]:
    with tempfile.TemporaryDirectory() as tmpdir:
        tmp_path = Path(tmpdir)
        package_path = tmp_path / "package.xml"
        destructive_path = tmp_path / "destructiveChanges.xml"
        write_package_xml(package_path, [], api_version)
        destructive_xml = generate_destructive_changes_xml(components, api_version)
        destructive_path.write_text(destructive_xml, encoding="utf-8")
        return sfdc_helpers.project_deploy_start(
            source_dir=None,
            manifest=package_path,
            org_alias=org_alias,
            dry_run=False,
            wait=10,
        )


def _destructive_summary(manifest: Optional[SfdcDestructiveManifest]) -> Dict[str, Any]:
    return {
        "fields": len(manifest.remove_fields) if manifest else 0,
        "validation_rules": len(manifest.remove_validation_rules) if manifest else 0,
        "permission_sets": len(manifest.remove_permission_sets) if manifest else 0,
        "record_types": len(manifest.remove_record_types) if manifest else 0,
    }


def _format_summary(compare: Dict[str, Any]) -> str:
    summary = compare.get("summary") or {}
    lines = [
        f"Components: +{summary.get('added', 0)} -{summary.get('removed', 0)} ~{summary.get('changed', 0)}",
    ]
    for type_name, stats in sorted((compare.get("by_type") or {}).items()):
        lines.append(
            f"{type_name}: +{stats.get('added', 0)} -{stats.get('removed', 0)} ~{stats.get('changed', 0)}"
        )
    return "\n".join(lines)
