"""Architect orchestrator for blueprint-driven projects."""

from __future__ import annotations

import hashlib
import json
import shlex
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable

from .approvals import ApprovalTokenStore
from .architect_llm import (
    generate_metadata_pack,
    generate_proposal,
    generate_proposal_markdown,
    generate_skeleton,
    sanitize_artifact_json,
    sanitize_artifact_text,
)
from .architect_state import ArchitectStateStore
from .blueprints import BlueprintLoader, render_phase_args, render_phase_command
from .contracts import PlanRisk, PlanSource, PlanStep, PlanV1
from .executor import PlanExecutor
from .models import ArchitectProject, BlueprintDefinition, ProjectPhaseResult, ProjectStatus, StepResult
from .policy import apply_policy_overlays, compute_policy_hash, generate_default_policy, load_policy
from .receipts import ReceiptWriter
from .state import resolve_state_dir
from .tools import AutopilotToolDefinition, ToolRegistry, get_default_registry
from .plays import evaluate_condition


PHASE_STATUS_MAP = {
    "discover": (ProjectStatus.DISCOVERING, ProjectStatus.DISCOVERED),
    "propose": (ProjectStatus.PROPOSING, ProjectStatus.PROPOSED),
    "deploy_sandbox": (ProjectStatus.DEPLOYING_SANDBOX, ProjectStatus.DEPLOYED_SANDBOX),
    "verify_sandbox": (ProjectStatus.VERIFYING_SANDBOX, ProjectStatus.VERIFIED_SANDBOX),
    "deploy_prod": (ProjectStatus.DEPLOYING_PROD, ProjectStatus.DEPLOYED_PROD),
}


class ArchitectOrchestrator:
    def __init__(
        self,
        *,
        state_store: ArchitectStateStore | None = None,
        blueprint_loader: BlueprintLoader | None = None,
        registry: ToolRegistry | None = None,
        policy_path: Path | None = None,
        state_dir: Path | None = None,
        runner: Callable[..., subprocess.CompletedProcess] | None = None,
    ) -> None:
        self.state_store = state_store or ArchitectStateStore()
        self.blueprint_loader = blueprint_loader or BlueprintLoader()
        self.registry = registry or get_default_registry()
        self.policy_path = policy_path
        self.state_dir_override = state_dir
        self._runner = runner or subprocess.run

    def list_blueprints(self) -> list[BlueprintDefinition]:
        return self.blueprint_loader.list_blueprints()

    def list_projects(self) -> list[ArchitectProject]:
        return self.state_store.list_projects()

    def init_project(self, blueprint_name: str, params: dict[str, Any]) -> ArchitectProject:
        variant = params.get("crm")
        if variant:
            params.setdefault("crm", variant)
            blueprint = self.blueprint_loader.resolve_blueprint(
                blueprint_name,
                str(variant),
                registry=self.registry,
            )
        else:
            blueprint = self._require_blueprint(blueprint_name)
            if blueprint.variants:
                raise ValueError("Blueprint requires crm variant. Pass --variant or -p crm=<variant>.")

        self._validate_required_params(blueprint, params)

        prod_only = bool(params.get("prod_only", False))
        if variant == "hubspot":
            sandbox_alias = str(params.get("hubspot_sandbox_profile") or "").strip()
            prod_alias = str(params.get("hubspot_prod_profile") or "").strip()
            if prod_only:
                if not prod_alias:
                    raise ValueError("hubspot_prod_profile is required for --prod-only.")
                sandbox_alias = prod_alias
                params["hubspot_sandbox_profile"] = sandbox_alias
            else:
                if not sandbox_alias or not prod_alias:
                    raise ValueError("hubspot_sandbox_profile and hubspot_prod_profile are required.")
        else:
            sandbox_alias = str(params.get("target_sandbox") or params.get("sandbox_org_alias") or "").strip()
            prod_alias = str(params.get("prod_org_alias") or "").strip()
            prod_only = False
            if not sandbox_alias or not prod_alias:
                raise ValueError("target_sandbox and prod_org_alias are required parameters.")

        project = self.state_store.create_project(
            blueprint_name=blueprint.name,
            parameters=params,
            sandbox_org_alias=sandbox_alias,
            prod_org_alias=prod_alias,
            variant=str(variant) if variant else None,
            prod_only=prod_only,
        )
        return project

    def run_phase(
        self,
        project_id: str,
        phase_name: str,
        *,
        approve_token: str | None = None,
        no_llm: bool = False,
    ) -> ProjectPhaseResult:
        project = self._require_project(project_id)
        if phase_name == "promote_prod":
            phase_name = "deploy_prod"
        blueprint = self._require_blueprint_for_project(project)
        if phase_name not in blueprint.phases:
            raise ValueError(f"Phase not found in blueprint: {phase_name}")

        status_start, status_end = PHASE_STATUS_MAP.get(phase_name, (project.status, project.status))
        self.state_store.update_status(project_id, status_start)
        started_at = _utc_now_iso()

        try:
            if phase_name == "discover":
                result = self._run_discover(project, blueprint, started_at)
            elif phase_name == "propose":
                result = self._run_propose(project, blueprint, started_at, no_llm=no_llm)
            elif phase_name in ("deploy_sandbox", "deploy_prod"):
                result = self._run_deploy_like(project, blueprint, phase_name, started_at, approve_token)
            elif phase_name == "verify_sandbox":
                result = self._run_verify(project, blueprint, started_at)
            else:
                raise ValueError(f"Unsupported phase: {phase_name}")

            self.state_store.update_status(project_id, status_end)
            self.state_store.record_phase_result(project_id, result)
            return result
        except Exception as exc:
            failed_result = ProjectPhaseResult(
                phase=phase_name,
                status="failed",
                started_at=started_at,
                completed_at=_utc_now_iso(),
                error=str(exc),
            )
            self.state_store.update_status(project_id, ProjectStatus.FAILED)
            self.state_store.record_phase_result(project_id, failed_result)
            raise

    def issue_token(self, project_id: str, phase_name: str, *, ttl_seconds: int = 600) -> str:
        project = self._require_project(project_id)
        if phase_name == "promote_prod":
            phase_name = "deploy_prod"
        blueprint = self._require_blueprint_for_project(project)
        phase = blueprint.phases.get(phase_name)
        if not phase:
            raise ValueError(f"Phase not found: {phase_name}")
        if not project.proposal_hash:
            raise ValueError("Proposal hash missing; run propose before issuing tokens.")

        policy, policy_hash, state_dir = self._load_policy()
        blueprint_hash = self.blueprint_loader.compute_blueprint_hash(
            blueprint.name,
            active_variant=blueprint.active_variant,
        )
        target_alias = self._target_org_alias(project, phase_name)
        binding, binding_hash = _build_token_binding(
            project_id=project.project_id,
            phase_name=phase_name,
            target_org_alias=target_alias,
            proposal_hash=project.proposal_hash,
            blueprint_hash=blueprint_hash,
            policy_hash=policy_hash,
        )

        store = ApprovalTokenStore(state_dir / "db" / "approval_tokens.sqlite")
        token = store.create_token(
            plan_hash=binding_hash,
            policy_hash=policy_hash,
            run_id=project.project_id,
            ttl_seconds=ttl_seconds,
            metadata=binding,
        ).token
        return token

    def _run_discover(
        self,
        project: ArchitectProject,
        blueprint: BlueprintDefinition,
        started_at: str,
    ) -> ProjectPhaseResult:
        phase = blueprint.phases["discover"]
        project_dir = Path(project.project_dir)
        discovery_dir = project_dir / "discovery"
        raw_dir = discovery_dir / "raw"
        logs_dir = discovery_dir / "logs"
        logs_dir.mkdir(parents=True, exist_ok=True)

        params = self._build_phase_params(
            project,
            phase_name="discover",
            extra_paths={
                "raw_dir": str(raw_dir),
                "discovery_dir": str(discovery_dir),
            },
            blueprint=blueprint,
        )
        step_results, artifacts = self._run_command_steps(phase, params, logs_dir)

        if project.variant == "hubspot":
            summary = _build_hubspot_discovery_summary(raw_dir, project, phase, params)
        else:
            summary = _build_salesforce_discovery_summary(raw_dir, project, phase, params)
        sanitized = sanitize_artifact_json(summary)
        discovery_path = discovery_dir / "discovery.json"
        discovery_path.write_text(json.dumps(sanitized, indent=2, ensure_ascii=True), encoding="utf-8")
        artifacts.append(str(discovery_path))

        return ProjectPhaseResult(
            phase="discover",
            status="success",
            started_at=started_at,
            completed_at=_utc_now_iso(),
            step_results=step_results,
            artifacts=artifacts,
        )

    def _run_propose(
        self,
        project: ArchitectProject,
        blueprint: BlueprintDefinition,
        started_at: str,
        *,
        no_llm: bool,
    ) -> ProjectPhaseResult:
        project_dir = Path(project.project_dir)
        discovery_path = project_dir / "discovery" / "discovery.json"
        if not discovery_path.exists():
            raise ValueError("Discovery output missing; run discover first.")
        discovery_json = json.loads(discovery_path.read_text(encoding="utf-8"))

        proposal_dir = project_dir / "proposal"
        proposal_dir.mkdir(parents=True, exist_ok=True)

        variant = blueprint.active_variant or project.variant or "salesforce"
        if no_llm:
            proposal = generate_skeleton(blueprint, discovery_json, project.parameters, variant=variant)
        else:
            proposal = generate_proposal(blueprint, discovery_json, project.parameters, variant=variant)

        proposal_hash = _hash_json(proposal)
        project.proposal_hash = proposal_hash
        self.state_store.update_project(project)

        proposal_path = proposal_dir / "proposal.json"
        proposal_path.write_text(json.dumps(proposal, indent=2, ensure_ascii=True), encoding="utf-8")

        generate_metadata_pack(proposal, project_dir, variant=variant)
        markdown = generate_proposal_markdown(proposal)
        proposal_md = proposal_dir / "PROPOSAL.md"
        proposal_md.write_text(sanitize_artifact_text(markdown), encoding="utf-8")

        return ProjectPhaseResult(
            phase="propose",
            status="success",
            started_at=started_at,
            completed_at=_utc_now_iso(),
            step_results=[],
            artifacts=[str(proposal_path), str(proposal_md)],
        )

    def _run_verify(
        self,
        project: ArchitectProject,
        blueprint: BlueprintDefinition,
        started_at: str,
    ) -> ProjectPhaseResult:
        phase = blueprint.phases["verify_sandbox"]
        project_dir = Path(project.project_dir)
        verify_dir = project_dir / "verify_sandbox"
        logs_dir = verify_dir / "logs"
        logs_dir.mkdir(parents=True, exist_ok=True)

        params = self._build_phase_params(
            project,
            phase_name="verify_sandbox",
            extra_paths={"verify_dir": str(verify_dir)},
            blueprint=blueprint,
        )

        step_results: list[StepResult] = []
        artifacts: list[str] = []

        # Tool steps (read-only verification)
        for step in phase.steps:
            if not step.tool:
                continue
            rendered_args = render_phase_args(step.args or {}, params)
            handler = self.registry.get_handler(step.tool)
            if not handler:
                raise RuntimeError(f"No handler registered for tool: {step.tool}")
            tool_def = self.registry.get_tool(step.tool)
            handler(step.model_copy(update={"args": rendered_args}), tool_def)
            step_results.append(
                StepResult(
                    name=step.name or step.label,
                    status="success",
                    duration_ms=0,
                )
            )

        # Command steps
        command_results, command_artifacts = self._run_command_steps(phase, params, logs_dir)
        step_results.extend(command_results)
        artifacts.extend(command_artifacts)

        verification = _build_verification_summary(project, verify_dir, params)
        verification_path = verify_dir / "verification.json"
        verification_path.write_text(json.dumps(verification, indent=2, ensure_ascii=True), encoding="utf-8")
        artifacts.append(str(verification_path))

        return ProjectPhaseResult(
            phase="verify_sandbox",
            status="success",
            started_at=started_at,
            completed_at=_utc_now_iso(),
            step_results=step_results,
            artifacts=artifacts,
        )

    def _run_deploy_like(
        self,
        project: ArchitectProject,
        blueprint: BlueprintDefinition,
        phase_name: str,
        started_at: str,
        approve_token: str | None,
    ) -> ProjectPhaseResult:
        phase = blueprint.phases[phase_name]
        if not project.proposal_hash:
            raise ValueError("Proposal hash missing; run propose first.")

        if phase_name == "deploy_prod":
            safety = blueprint.safety or None
            if safety and safety.requires_sandbox_verification:
                verify_result = project.phase_results.get("verify_sandbox")
                if project.status != ProjectStatus.VERIFIED_SANDBOX and not (
                    verify_result and verify_result.status == "success"
                ):
                    raise ValueError("Sandbox verification required before promotion.")

        policy, policy_hash, state_dir = self._load_policy()
        blueprint_hash = self.blueprint_loader.compute_blueprint_hash(
            blueprint.name,
            active_variant=blueprint.active_variant,
        )
        target_alias = self._target_org_alias(project, phase_name)

        binding, binding_hash = _build_token_binding(
            project_id=project.project_id,
            phase_name=phase_name,
            target_org_alias=target_alias,
            proposal_hash=project.proposal_hash,
            blueprint_hash=blueprint_hash,
            policy_hash=policy_hash,
        )

        if phase.approval_required:
            if not approve_token:
                raise ValueError("Approval token required for this phase.")
            store = ApprovalTokenStore(state_dir / "db" / "approval_tokens.sqlite")
            if not store.validate_token(approve_token, binding_hash, policy_hash, project.project_id):
                raise ValueError("Invalid or expired approval token.")
            store.consume_token(
                approve_token,
                binding_hash,
                policy_hash,
                project.project_id,
                metadata={"action": "approve", **binding},
            )

        plan = self._build_plan_for_phase(project, phase, phase_name, blueprint)
        executor = PlanExecutor(policy=policy, registry=self.registry, state_dir=state_dir)
        receipt = executor.execute(plan, approve_token=approve_token)
        writer = ReceiptWriter(state_dir)
        receipt_dir = writer.write(receipt, plan)

        project.receipt_ids[phase_name] = receipt.run_id
        self.state_store.update_project(project)

        return ProjectPhaseResult(
            phase=phase_name,
            status="success",
            started_at=started_at,
            completed_at=_utc_now_iso(),
            step_results=[],
            artifacts=[str(receipt_dir)],
            approval_token=approve_token,
        )

    def _run_command_steps(
        self,
        phase: Any,
        params: dict[str, Any],
        logs_dir: Path,
    ) -> tuple[list[StepResult], list[str]]:
        step_results: list[StepResult] = []
        artifacts: list[str] = []
        for step in phase.steps:
            if not step.command:
                continue
            step_name = step.name or _safe_name(step.label)
            rendered_command = render_phase_command(step.command, params)
            rendered_args = render_phase_args(step.args or {}, params)
            output_path = rendered_args.get("output_path")
            log_file = logs_dir / f"{step_name}.log"

            start_time = datetime.now(timezone.utc)
            try:
                if step.shell:
                    result = self._runner(
                        rendered_command,
                        shell=True,
                        capture_output=True,
                        text=True,
                        timeout=600,
                    )
                else:
                    result = self._runner(
                        shlex.split(rendered_command),
                        shell=False,
                        capture_output=True,
                        text=True,
                        timeout=600,
                    )
                duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
                log_file.write_text(
                    _build_log_text(rendered_command, step.shell, result),
                    encoding="utf-8",
                )
                if output_path:
                    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
                    Path(output_path).write_text(result.stdout or "", encoding="utf-8")
                    artifacts.append(str(output_path))

                if result.returncode != 0:
                    step_results.append(
                        StepResult(
                            name=step_name,
                            status="failed",
                            duration_ms=duration_ms,
                            error=result.stderr[-500:] if result.stderr else None,
                            log_file=str(log_file),
                        )
                    )
                    break

                step_results.append(
                    StepResult(
                        name=step_name,
                        status="success",
                        duration_ms=duration_ms,
                        log_file=str(log_file),
                    )
                )
            except subprocess.TimeoutExpired:
                step_results.append(
                    StepResult(
                        name=step_name,
                        status="failed",
                        duration_ms=600000,
                        error="Command timed out after 10 minutes",
                        log_file=str(log_file),
                    )
                )
                break
        return step_results, artifacts

    def _build_plan_for_phase(
        self,
        project: ArchitectProject,
        phase: Any,
        phase_name: str,
        blueprint: BlueprintDefinition | None = None,
    ) -> PlanV1:
        params = self._build_phase_params(project, phase_name=phase_name, blueprint=blueprint)
        steps: list[PlanStep] = []
        max_risk = 0
        blast_radius = None
        for idx, step in enumerate(phase.steps):
            if not step.tool:
                continue
            rendered_args = render_phase_args(step.args or {}, params)
            tool_def = self.registry.get_tool(step.tool)
            risk_level = tool_def.risk_level if tool_def else 0
            max_risk = max(max_risk, risk_level)
            blast_radius = blast_radius or (tool_def.blast_radius if tool_def else None)
            steps.append(
                PlanStep(
                    step_id=f"{phase_name}_{idx+1}",
                    tool=step.tool,
                    args=rendered_args,
                    risk_level=risk_level,
                    prechecks=list(tool_def.default_prechecks) if tool_def else [],
                    postchecks=list(tool_def.default_postchecks) if tool_def else [],
                )
            )

        run_id = f"architect_{project.project_id}_{phase_name}_{_utc_now_slug()}"
        plan = PlanV1(
            run_id=run_id,
            source=PlanSource(type="architect_phase", scan_id=project.project_id),
            summary=f"{project.blueprint_name}:{phase_name}",
            risk=PlanRisk(max_risk_level=max_risk, blast_radius=blast_radius),
            steps=steps,
        )
        return plan

    def _target_org_alias(self, project: ArchitectProject, phase_name: str) -> str:
        if phase_name == "deploy_prod":
            return project.prod_org_alias
        return project.sandbox_org_alias

    def _build_phase_params(
        self,
        project: ArchitectProject,
        *,
        phase_name: str,
        extra_paths: dict[str, str] | None = None,
        blueprint: BlueprintDefinition | None = None,
    ) -> dict[str, Any]:
        params = dict(project.parameters or {})
        if blueprint:
            params = self._filter_params_by_when(blueprint, params)
        elif project.variant:
            try:
                params = self._filter_params_by_when(self._require_blueprint_for_project(project), params)
            except Exception:
                pass
        params.update(
            {
                "project_id": project.project_id,
                "project_dir": project.project_dir,
                "sandbox_org_alias": project.sandbox_org_alias,
                "prod_org_alias": project.prod_org_alias,
                "target_sandbox": project.sandbox_org_alias,
                "timestamp": _utc_now_slug(),
                "phase_name": phase_name,
                "crm_variant": project.variant or params.get("crm") or "salesforce",
                "hubspot_sandbox_profile": params.get("hubspot_sandbox_profile")
                or (project.sandbox_org_alias if project.variant == "hubspot" else ""),
                "hubspot_prod_profile": params.get("hubspot_prod_profile")
                or (project.prod_org_alias if project.variant == "hubspot" else ""),
                "prod_only": project.prod_only,
            }
        )
        if extra_paths:
            params.update(extra_paths)
        return params

    def _validate_required_params(self, blueprint: BlueprintDefinition, params: dict[str, Any]) -> None:
        missing = []
        for param in blueprint.parameters:
            if param.when and not evaluate_condition(param.when, params):
                continue
            if param.required and param.name not in params:
                missing.append(param.name)
        if missing:
            raise ValueError(f"Missing required parameters: {', '.join(missing)}")

    def _filter_params_by_when(
        self,
        blueprint: BlueprintDefinition,
        params: dict[str, Any],
    ) -> dict[str, Any]:
        filtered = dict(params)
        for param in blueprint.parameters:
            if param.when and not evaluate_condition(param.when, params):
                filtered.pop(param.name, None)
        return filtered

    def _require_project(self, project_id: str) -> ArchitectProject:
        project = self.state_store.get_project(project_id)
        if not project:
            raise ValueError(f"Project not found: {project_id}")
        return project

    def _require_blueprint(self, name: str) -> BlueprintDefinition:
        blueprint = self.blueprint_loader.get_blueprint(name)
        if not blueprint:
            raise ValueError(f"Blueprint not found: {name}")
        return blueprint

    def _require_blueprint_for_project(self, project: ArchitectProject) -> BlueprintDefinition:
        if project.variant:
            return self.blueprint_loader.resolve_blueprint(
                project.blueprint_name,
                project.variant,
                registry=self.registry,
            )
        return self._require_blueprint(project.blueprint_name)

    def _load_policy(self) -> tuple[Any, str, Path]:
        policy_path = self.policy_path or Path("autopilot_policy.yaml")
        if policy_path.exists():
            policy = load_policy(policy_path)
        else:
            policy = generate_default_policy("local")
        state_dir = self.state_dir_override or resolve_state_dir(policy, create=True)
        policy = apply_policy_overlays(policy, state_dir)
        policy_hash = compute_policy_hash(policy)
        return policy, policy_hash, state_dir


def _build_log_text(command: str, shell_mode: bool, result: subprocess.CompletedProcess) -> str:
    return "\n".join(
        [
            f"Command: {command}",
            f"Shell: {shell_mode}",
            f"Exit code: {result.returncode}",
            "",
            "=== STDOUT ===",
            result.stdout or "",
            "",
            "=== STDERR ===",
            result.stderr or "",
            "",
        ]
    )


def _safe_name(value: str) -> str:
    text = "".join(ch if ch.isalnum() or ch in ("_", "-") else "_" for ch in value)
    return text.strip("_") or "step"


def _utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _utc_now_slug() -> str:
    return datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")


def _hash_json(payload: dict[str, Any]) -> str:
    raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
    return f"sha256:{hashlib.sha256(raw.encode('utf-8')).hexdigest()}"


def _build_token_binding(
    *,
    project_id: str,
    phase_name: str,
    target_org_alias: str,
    proposal_hash: str,
    blueprint_hash: str,
    policy_hash: str,
) -> tuple[dict[str, Any], str]:
    binding = {
        "project_id": project_id,
        "phase_name": phase_name,
        "target_org_alias": target_org_alias,
        "proposal_hash": proposal_hash,
        "blueprint_hash": blueprint_hash,
        "policy_hash": policy_hash,
    }
    raw = json.dumps(binding, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
    binding_hash = f"sha256:{hashlib.sha256(raw.encode('utf-8')).hexdigest()}"
    return binding, binding_hash


def _build_salesforce_discovery_summary(
    raw_dir: Path,
    project: ArchitectProject,
    phase: Any,
    params: dict[str, Any],
) -> dict[str, Any]:
    outputs = _collect_step_outputs(phase, params)
    lead_describe = _load_json_from_outputs(outputs, raw_dir, ["lead_describe", "lead_describe.json", "lead"])
    account_describe = _load_json_from_outputs(outputs, raw_dir, ["account_describe", "account_describe.json", "account"])
    unconverted = _load_json_from_outputs(
        outputs,
        raw_dir,
        ["unconverted_lead_count", "unconverted_lead_count.json", "lead_count"],
    )
    unmatched = _load_json_from_outputs(
        outputs,
        raw_dir,
        ["unmatched_lead_sample", "unmatched_lead_sample.json", "unmatched"],
    )
    l2a = _load_json_from_outputs(outputs, raw_dir, ["l2a_match_health", "l2a_match_health.json", "match_health"])

    l2a_summary = _summarize_l2a(l2a)
    unmatched_count = _extract_count(unconverted)
    total_count = l2a_summary.get("total_count") or unmatched_count
    match_rate = l2a_summary.get("match_rate")
    if match_rate is None and isinstance(total_count, int):
        matched = l2a_summary.get("matched_count")
        if isinstance(matched, int) and total_count > 0:
            match_rate = matched / total_count

    return {
        "project_id": project.project_id,
        "blueprint": project.blueprint_name,
        "crm_variant": project.variant or "salesforce",
        "generated_at": _utc_now_iso(),
        "environment": {
            "type": "salesforce",
            "identifier": project.sandbox_org_alias,
        },
        "schema_snippets": {
            "primary_object": _describe_snippet(lead_describe),
            "target_object": _describe_snippet(account_describe),
        },
        "health_metrics": {
            "match_rate": match_rate,
            "unmatched_count": unmatched_count,
            "total_count": total_count,
        },
        "raw_artifacts": {
            "lead_describe": _path_str_or_empty(outputs, "lead_describe", raw_dir / "lead_describe.json"),
            "account_describe": _path_str_or_empty(outputs, "account_describe", raw_dir / "account_describe.json"),
            "unconverted_lead_count": _path_str_or_empty(
                outputs, "unconverted_lead_count", raw_dir / "unconverted_lead_count.json"
            ),
            "unmatched_lead_sample": _path_str_or_empty(
                outputs, "unmatched_lead_sample", raw_dir / "unmatched_lead_sample.json"
            ),
            "l2a_match_health": _path_str_or_empty(
                outputs, "l2a_match_health", raw_dir / "l2a_match_health.json"
            ),
        },
        "parameters": params,
    }


def _build_hubspot_discovery_summary(
    raw_dir: Path,
    project: ArchitectProject,
    phase: Any,
    params: dict[str, Any],
) -> dict[str, Any]:
    outputs = _collect_step_outputs(phase, params)
    contacts_schema = _load_json_from_outputs(
        outputs,
        raw_dir,
        ["contacts_schema", "contact_schema", "contacts", "contact_describe"],
    )
    companies_schema = _load_json_from_outputs(
        outputs,
        raw_dir,
        ["companies_schema", "company_schema", "companies", "company_describe"],
    )
    unassociated = _load_json_from_outputs(
        outputs,
        raw_dir,
        ["unassociated_contacts", "unassociated_contacts.json", "unmatched_contacts", "unmatched"],
    )
    health_payload = _load_json_from_outputs(
        outputs,
        raw_dir,
        ["association_health", "association_health.json", "match_health"],
    )

    unmatched_count = _extract_hubspot_count(unassociated)
    total_count = _extract_hubspot_count(health_payload) or None
    match_rate = _extract_hubspot_metric(health_payload, "match_rate")
    if match_rate is None and isinstance(total_count, int) and isinstance(unmatched_count, int) and total_count > 0:
        match_rate = 1 - (unmatched_count / total_count)

    return {
        "project_id": project.project_id,
        "blueprint": project.blueprint_name,
        "crm_variant": project.variant or "hubspot",
        "generated_at": _utc_now_iso(),
        "environment": {
            "type": "hubspot",
            "identifier": f"profile:{project.sandbox_org_alias}",
        },
        "schema_snippets": {
            "primary_object": _hubspot_schema_snippet(contacts_schema, "contacts"),
            "target_object": _hubspot_schema_snippet(companies_schema, "companies"),
        },
        "health_metrics": {
            "match_rate": match_rate,
            "unmatched_count": unmatched_count,
            "total_count": total_count,
        },
        "raw_artifacts": {
            "contacts_schema": _path_str_or_empty(outputs, "contacts_schema", raw_dir / "contacts_schema.json"),
            "companies_schema": _path_str_or_empty(outputs, "companies_schema", raw_dir / "companies_schema.json"),
            "unassociated_contacts": _path_str_or_empty(
                outputs, "unassociated_contacts", raw_dir / "unassociated_contacts.json"
            ),
            "association_health": _path_str_or_empty(
                outputs, "association_health", raw_dir / "association_health.json"
            ),
        },
        "parameters": params,
    }


def _build_verification_summary(
    project: ArchitectProject,
    verify_dir: Path,
    params: dict[str, Any],
) -> dict[str, Any]:
    discovery_path = Path(project.project_dir) / "discovery" / "discovery.json"
    discovery = _load_json(discovery_path) if discovery_path.exists() else {}
    post_scan = _load_json(verify_dir / "l2a_match_health_post.json")
    post_summary = _summarize_l2a(post_scan)

    pre_rate = (discovery.get("l2a_health") or {}).get("match_rate")
    post_rate = post_summary.get("match_rate")
    delta = None
    if isinstance(pre_rate, (int, float)) and isinstance(post_rate, (int, float)):
        delta = post_rate - pre_rate

    return {
        "project_id": project.project_id,
        "generated_at": _utc_now_iso(),
        "pre_match_rate": pre_rate,
        "post_match_rate": post_rate,
        "delta_match_rate": delta,
        "post_scan": post_summary,
        "parameters": params,
    }


def _load_json(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def _collect_step_outputs(phase: Any, params: dict[str, Any]) -> dict[str, Path]:
    outputs: dict[str, Path] = {}
    for step in phase.steps:
        if not step.args:
            continue
        rendered = render_phase_args(step.args or {}, params)
        output_path = rendered.get("output_path") or rendered.get("output")
        if not output_path:
            continue
        path = Path(str(output_path))
        key = step.name or _safe_name(step.label)
        outputs[key] = path
        outputs[path.name] = path
    return outputs


def _load_json_from_outputs(
    outputs: dict[str, Path],
    raw_dir: Path,
    patterns: list[str],
) -> dict[str, Any]:
    for pattern in patterns:
        if pattern in outputs:
            return _load_json(outputs[pattern])
    for key, path in outputs.items():
        if any(pattern in key for pattern in patterns):
            return _load_json(path)
    for pattern in patterns:
        if pattern.endswith(".json"):
            candidate = raw_dir / pattern
            if candidate.exists():
                return _load_json(candidate)
    return {}


def _path_str_or_empty(outputs: dict[str, Path], key: str, fallback: Path) -> str:
    if key in outputs:
        return str(outputs[key])
    return str(fallback) if fallback.exists() else ""


def _describe_snippet(payload: dict[str, Any], max_fields: int = 20) -> dict[str, Any]:
    result = payload.get("result") or {}
    fields = result.get("fields") or []
    snippet = []
    for field in fields:
        name = field.get("name")
        if not name:
            continue
        snippet.append(
            {
                "name": name,
                "type": field.get("type"),
                "length": field.get("length"),
                "updateable": field.get("updateable"),
            }
        )
        if len(snippet) >= max_fields:
            break
    return {
        "object": result.get("name"),
        "field_count": len(fields),
        "fields": snippet,
    }


def _hubspot_schema_snippet(payload: dict[str, Any], object_name: str, max_fields: int = 20) -> dict[str, Any]:
    properties = []
    if isinstance(payload, dict):
        if isinstance(payload.get("properties"), list):
            properties = payload.get("properties") or []
        elif isinstance(payload.get("result"), dict) and isinstance(payload["result"].get("properties"), list):
            properties = payload["result"].get("properties") or []
        elif isinstance(payload.get("properties"), dict):
            properties = payload.get("properties", {}).get(object_name) or []

    snippet = []
    for prop in properties:
        if not isinstance(prop, dict):
            continue
        name = prop.get("name") or prop.get("propertyName")
        if not name:
            continue
        snippet.append(
            {
                "name": name,
                "label": prop.get("label") or name,
                "type": prop.get("type") or prop.get("fieldType"),
                "group_name": prop.get("groupName"),
                "is_custom": prop.get("isCustom"),
            }
        )
        if len(snippet) >= max_fields:
            break

    return {
        "object": object_name,
        "field_count": len(properties),
        "fields": snippet,
    }


def _extract_hubspot_count(payload: dict[str, Any]) -> int | None:
    if not isinstance(payload, dict):
        return None
    for key in ("total", "totalCount", "total_count", "count"):
        value = payload.get(key)
        if isinstance(value, int):
            return value
        if isinstance(value, str) and value.isdigit():
            return int(value)
    result = payload.get("result") or {}
    if isinstance(result, dict):
        for key in ("total", "totalCount", "total_count", "count"):
            value = result.get(key)
            if isinstance(value, int):
                return value
    results = payload.get("results")
    if isinstance(results, list):
        return len(results)
    return None


def _extract_hubspot_metric(payload: dict[str, Any], key: str) -> float | None:
    if not isinstance(payload, dict):
        return None
    for candidate in (payload, payload.get("result") or {}, payload.get("summary") or {}):
        if isinstance(candidate, dict) and key in candidate:
            value = candidate.get(key)
            if isinstance(value, (int, float)):
                return float(value)
    return None


def _extract_count(payload: dict[str, Any]) -> int | None:
    result = payload.get("result") or {}
    records = result.get("records") or []
    if records and isinstance(records, list):
        record = records[0] if records else {}
        for key in ("expr0", "count", "totalSize"):
            if key in record:
                try:
                    return int(record.get(key))
                except Exception:
                    pass
    total = result.get("totalSize")
    if isinstance(total, int):
        return total
    return None


def _summarize_unmatched(payload: dict[str, Any]) -> dict[str, Any]:
    result = payload.get("result") or {}
    records = result.get("records") or []
    if not isinstance(records, list):
        records = []
    missing_company = 0
    missing_email = 0
    missing_website = 0
    for row in records:
        if not row.get("Company"):
            missing_company += 1
        if not row.get("Email"):
            missing_email += 1
        if not row.get("Website"):
            missing_website += 1
    return {
        "sample_size": len(records),
        "missing_company": missing_company,
        "missing_email": missing_email,
        "missing_website": missing_website,
    }


def _summarize_l2a(payload: dict[str, Any]) -> dict[str, Any]:
    evidence = payload.get("evidence") or payload.get("result", {}).get("evidence") or []
    summary_fields: dict[str, Any] = {}
    if isinstance(evidence, list):
        for row in evidence:
            if row.get("record_id") == "summary" or row.get("reason_code") == "MATCH_RATE":
                summary_fields = row.get("fields") or {}
                break
    return {
        "match_rate": summary_fields.get("match_rate"),
        "matched_count": summary_fields.get("matched_count"),
        "total_count": summary_fields.get("total_count"),
        "threshold": summary_fields.get("threshold"),
        "missing_domain_pct": summary_fields.get("missing_domain_pct"),
        "personal_email_pct": summary_fields.get("personal_email_pct"),
        "normalization_issue_pct": summary_fields.get("normalization_issue_pct"),
    }


def _domain_distribution(unmatched_payload: dict[str, Any], l2a_payload: dict[str, Any]) -> dict[str, Any]:
    counts: dict[str, int] = {}
    records = (unmatched_payload.get("result") or {}).get("records") or []
    if isinstance(records, list):
        for row in records:
            domain = _extract_domain(row.get("Email") or row.get("Website"))
            if not domain:
                continue
            counts[domain] = counts.get(domain, 0) + 1

    if not counts:
        evidence = l2a_payload.get("evidence") or []
        if isinstance(evidence, list):
            for row in evidence:
                fields = row.get("fields") or {}
                domain = _extract_domain(fields.get("Email") or fields.get("Website") or fields.get("Domain"))
                if not domain:
                    continue
                counts[domain] = counts.get(domain, 0) + 1

    top = sorted(counts.items(), key=lambda kv: kv[1], reverse=True)[:10]
    return {"top_domains": [{"domain": d, "count": c} for d, c in top]}


def _extract_domain(value: Any) -> str | None:
    if not value:
        return None
    text = str(value).strip().lower()
    if "@" in text:
        text = text.split("@", 1)[1]
    if text.startswith("http://") or text.startswith("https://"):
        text = text.split("://", 1)[1]
    text = text.split("/")[0]
    if text.startswith("www."):
        text = text[4:]
    if "." not in text:
        return None
    return text


__all__ = ["ArchitectOrchestrator"]
