"""Quote draft planning and quote create handlers for Deal Desk."""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any

from g_gremlin import sfdc as sfdc_helpers
from g_gremlin.autopilot.contracts import PlanStep
from g_gremlin.autopilot.tools import AutopilotToolDefinition

from .eventing import (
    build_pattern_update_event,
    resolve_crm_org_id,
    save_pattern_update_events,
    update_context_for_line_items,
    update_context_for_quote_create,
    update_context_for_quote_rollback,
    utc_now_iso,
)
from .handlers import _fetch_opportunity_snapshot, handle_dealdesk_quote_readiness
from .profiles import FieldMappingCandidate, QuoteEnvironmentProfileV1
from .quote_plan import QuoteDraftPlanResult, QuoteDraftPlanV1, QuoteLineDraft, build_quote_draft_plan
from .readiness import QuoteReadinessResult
from .store import DealDeskStore


_TERMINAL_QUOTE_STATUSES = {"Denied", "Rejected", "Cancelled", "Expired"}
_LINE_ITEM_BLOCKED_QUOTE_STATUSES = _TERMINAL_QUOTE_STATUSES | {"Accepted"}


def handle_dealdesk_quote_plan(
    step: PlanStep,
    tool_def: AutopilotToolDefinition | None,
) -> dict[str, object]:
    del tool_def
    planning_state = _build_planning_state(step, persist_plan=True)
    return {
        "targets": planning_state["targets"],
        "before": planning_state["before"],
        "after": _render_planning_result(
            planning=planning_state["planning"],
            readiness_after=planning_state["readiness_after"],
            quote_plan_path=planning_state["quote_plan_path"],
        ),
    }


def handle_dealdesk_quote_create(
    step: PlanStep,
    tool_def: AutopilotToolDefinition | None,
) -> dict[str, object]:
    del tool_def
    args = step.args or {}
    opportunity_id = _resolve_opportunity_id(args)
    if not opportunity_id:
        raise RuntimeError("Missing opportunity_id for dealdesk quote creation.")

    org_alias = str(args.get("org_alias") or "").strip() or None
    crm_org_id = resolve_crm_org_id(org_alias=org_alias, crm_org_id=args.get("crm_org_id"))
    store = DealDeskStore(args.get("state_dir"))
    quote_plan_path = store.quote_draft_plan_path(
        opportunity_id=opportunity_id,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )
    persisted_plan = store.load_quote_draft_plan(
        opportunity_id=opportunity_id,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )
    if persisted_plan is None:
        raise RuntimeError(
            "Quote creation requires a persisted QuoteDraftPlanV1. Run dealdesk.quote.plan first."
        )

    planning_state = _build_planning_state(
        step,
        quote_request=_quote_request_from_plan(persisted_plan),
        persist_plan=False,
    )
    planning = planning_state["planning"]
    readiness_after = planning_state["readiness_after"]
    environment_profile = planning_state["environment_profile"]
    context = planning_state["context"]

    approved_by = str(args.get("approved_by") or args.get("actor") or "").strip() or None
    approval_note = str(args.get("approval_note") or args.get("rationale") or "").strip() or None
    allow_review_required = bool(args.get("allow_review_required"))
    allow_insufficient_evidence = bool(args.get("allow_insufficient_evidence"))

    blocking_reasons = list(planning.blocking_reasons)
    if persisted_plan.approval_required and not approved_by:
        blocking_reasons.append(
            "DD_APPROVAL_REQUIRED: QuoteDraftPlanV1 requires approved_by before quote creation."
        )
    if planning.readiness_mode == "review_required" and not allow_review_required:
        blocking_reasons.append(
            "DD_REVIEW_REQUIRED_OVERRIDE: readiness_mode review_required requires explicit allow_review_required."
        )

    opportunity_snapshot = _fetch_opportunity_snapshot(
        opportunity_id=opportunity_id,
        environment_profile=environment_profile,
        org_alias=org_alias,
    )
    if not opportunity_snapshot:
        blocking_reasons.append(
            f"DD_OPPORTUNITY_NOT_FOUND: Opportunity {opportunity_id} could not be loaded before quote creation."
        )

    duplicate_quotes = _find_active_quotes(opportunity_id=opportunity_id, org_alias=org_alias)
    if duplicate_quotes:
        duplicate_refs = ", ".join(
            f"{row.get('Id')} ({row.get('Status') or 'Unknown'})" for row in duplicate_quotes
        )
        blocking_reasons.append(
            f"DD_DUPLICATE_ACTIVE_QUOTE: Active quote records already exist on the opportunity: {duplicate_refs}."
        )

    pricebook_id, pricebook_source = _resolve_pricebook_id(
        opportunity_snapshot=opportunity_snapshot,
        environment_profile=environment_profile,
        org_alias=org_alias,
    )
    if not pricebook_id:
        blocking_reasons.append(
            "DD_PRICEBOOK_UNRESOLVED: No active pricebook could be resolved for quote creation."
        )

    quote_field_map = _describe_field_map("Quote", org_alias=org_alias)
    if not quote_field_map:
        blocking_reasons.append("DD_QUOTE_DESCRIBE_UNAVAILABLE: Quote describe returned no usable fields.")

    quote_values = _build_quote_values(
        plan=persisted_plan,
        context=context,
        opportunity_snapshot=opportunity_snapshot,
        environment_profile=environment_profile,
        quote_field_map=quote_field_map,
        pricebook_id=pricebook_id,
    )
    missing_required_fields = _missing_required_fields(quote_field_map, quote_values)
    if missing_required_fields:
        blocking_reasons.append(
            "DD_MISSING_REQUIRED_QUOTE_FIELDS: Missing required Quote fields for create: "
            + ", ".join(missing_required_fields)
            + "."
        )

    if blocking_reasons:
        return _render_create_result(
            opportunity_id=opportunity_id,
            quote_plan_path=str(quote_plan_path),
            quote_plan=persisted_plan,
            readiness_after=readiness_after,
            planning=planning,
            status="blocked",
            created=False,
            blocking_reasons=_dedupe_strings(blocking_reasons),
            quote_values=quote_values,
            pricebook_id=pricebook_id,
            pricebook_source=pricebook_source,
            duplicate_quotes=duplicate_quotes,
        )

    result = sfdc_helpers.create_record("Quote", quote_values, org_alias=org_alias)
    quote_id = _record_id_from_result(result)
    if not quote_id:
        raise RuntimeError("Quote creation did not return a Salesforce Quote ID.")

    created_quote = _load_created_quote(quote_id=quote_id, org_alias=org_alias)
    if not created_quote:
        raise RuntimeError(f"Quote {quote_id} could not be loaded after creation.")
    if str(created_quote.get("OpportunityId") or "").strip() != opportunity_id:
        raise RuntimeError(
            f"Quote {quote_id} postcheck failed: OpportunityId did not match {opportunity_id}."
        )
    if pricebook_id and created_quote.get("Pricebook2Id") not in (None, "", pricebook_id):
        raise RuntimeError(
            f"Quote {quote_id} postcheck failed: Pricebook2Id did not match resolved pricebook {pricebook_id}."
        )

    observed_at = utc_now_iso()
    run_id = str(args.get("run_id") or args.get("action_details", {}).get("run_id") or "").strip() or None
    event = build_pattern_update_event(
        source_type="quote_receipt",
        crm_org_id=crm_org_id,
        opportunity_id=opportunity_id,
        source_ref=f"dealdesk.quote.create:{opportunity_id}:{quote_id}",
        observed_at=observed_at,
        source_receipt_id=run_id,
        raw_record={**opportunity_snapshot, **quote_values},
        extracted_motion_id=context.inferred_motion_id,
        confidence=float(persisted_plan.confidence),
        payment_terms=persisted_plan.requested_payment_terms,
        template_key=persisted_plan.requested_template_key,
        discount_pct=persisted_plan.requested_discount_pct,
        outcome="created",
        custom={
            "engine": persisted_plan.engine,
            "header_only": True,
            "pricebook_id": pricebook_id,
            "pricebook_source": pricebook_source,
            "quote_id": quote_id,
            "quote_request_id": persisted_plan.quote_request_id,
            "approved_by": approved_by,
            "allow_insufficient_evidence": allow_insufficient_evidence,
        },
        evidence_ids=[*persisted_plan.evidence_ids, opportunity_id, quote_id],
    )
    event_paths = save_pattern_update_events(
        store=store,
        events=[event],
        org_alias=org_alias,
        refresh_profile=True,
    )
    context_path = update_context_for_quote_create(
        store=store,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        opportunity_id=opportunity_id,
        quote_id=quote_id,
        engine=persisted_plan.engine,
        status=str(created_quote.get("Status") or quote_values.get("Status") or "Draft"),
        observed_at=observed_at,
        assumptions=persisted_plan.assumptions,
        approved_by=approved_by,
        approval_note=approval_note,
        receipt_id=run_id,
        account_id=str(opportunity_snapshot.get("AccountId") or "").strip() or None,
    )
    return _render_create_result(
        opportunity_id=opportunity_id,
        quote_plan_path=str(quote_plan_path),
        quote_plan=persisted_plan,
        readiness_after=readiness_after,
        planning=planning,
        status="created",
        created=True,
        blocking_reasons=[],
        quote_values=quote_values,
        pricebook_id=pricebook_id,
        pricebook_source=pricebook_source,
        duplicate_quotes=[],
        quote_id=quote_id,
        quote=created_quote,
        result=result,
        context_path=str(context_path),
        pattern_event_paths=[str(path) for path in event_paths],
    )


def handle_dealdesk_quote_line_items_upsert(
    step: PlanStep,
    tool_def: AutopilotToolDefinition | None,
) -> dict[str, object]:
    del tool_def
    args = step.args or {}
    quote_id = _resolve_quote_id(args)
    if not quote_id:
        raise RuntimeError("Missing quote_id for dealdesk quote line-item upsert.")

    org_alias = str(args.get("org_alias") or "").strip() or None
    crm_org_id = resolve_crm_org_id(org_alias=org_alias, crm_org_id=args.get("crm_org_id"))
    store = DealDeskStore(args.get("state_dir"))

    quote = _load_created_quote(quote_id=quote_id, org_alias=org_alias)
    opportunity_id = _resolve_opportunity_id(args)
    if not quote:
        return _render_line_item_result(
            quote_id=quote_id,
            opportunity_id=opportunity_id or None,
            quote_plan=None,
            quote_plan_path=None,
            readiness_after={},
            planning=None,
            status="blocked",
            created=False,
            blocking_reasons=[f"DD_QUOTE_NOT_FOUND: Quote {quote_id} could not be loaded before line-item upsert."],
            resolutions=[],
            quote_line_item_values=[],
            existing_quote_line_items=[],
            created_line_item_ids=[],
            created_quote_line_items=[],
        )

    quote_opportunity_id = str(quote.get("OpportunityId") or "").strip()
    if opportunity_id and quote_opportunity_id and opportunity_id != quote_opportunity_id:
        return _render_line_item_result(
            quote_id=quote_id,
            opportunity_id=quote_opportunity_id,
            quote_plan=None,
            quote_plan_path=None,
            readiness_after={},
            planning=None,
            status="blocked",
            created=False,
            blocking_reasons=[
                "DD_QUOTE_OPPORTUNITY_MISMATCH: Provided opportunity_id does not match the quote's OpportunityId."
            ],
            resolutions=[],
            quote_line_item_values=[],
            existing_quote_line_items=[],
            created_line_item_ids=[],
            created_quote_line_items=[],
        )
    opportunity_id = opportunity_id or quote_opportunity_id
    if not opportunity_id:
        raise RuntimeError("Quote line-item upsert could not determine the source opportunity_id.")

    quote_status = str(quote.get("Status") or "").strip()
    if quote_status in _LINE_ITEM_BLOCKED_QUOTE_STATUSES:
        return _render_line_item_result(
            quote_id=quote_id,
            opportunity_id=opportunity_id,
            quote_plan=None,
            quote_plan_path=None,
            readiness_after={},
            planning=None,
            status="blocked",
            created=False,
            blocking_reasons=[
                f"DD_TERMINAL_QUOTE_STATUS: Quote {quote_id} is already in terminal status {quote_status}."
            ],
            resolutions=[],
            quote_line_item_values=[],
            existing_quote_line_items=[],
            created_line_item_ids=[],
            created_quote_line_items=[],
        )

    quote_plan_path = store.quote_draft_plan_path(
        opportunity_id=opportunity_id,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )
    persisted_plan = store.load_quote_draft_plan(
        opportunity_id=opportunity_id,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )
    if persisted_plan is None:
        raise RuntimeError(
            "Quote line-item upsert requires a persisted QuoteDraftPlanV1. Run dealdesk.quote.plan first."
        )
    if not persisted_plan.line_items:
        raise RuntimeError("Persisted QuoteDraftPlanV1 does not contain any line_items to upsert.")

    planning_state = _build_planning_state(
        _step_with_opportunity_id(step, opportunity_id),
        quote_request=_quote_request_from_plan(persisted_plan),
        persist_plan=False,
    )
    planning = planning_state["planning"]
    readiness_after = planning_state["readiness_after"]
    context = planning_state["context"]

    blocking_reasons = list(planning.blocking_reasons)
    pricebook_id = str(quote.get("Pricebook2Id") or "").strip() or None
    if not pricebook_id:
        blocking_reasons.append(
            f"DD_QUOTE_PRICEBOOK_MISSING: Quote {quote_id} does not have Pricebook2Id populated."
        )

    qli_field_map = _describe_field_map("QuoteLineItem", org_alias=org_alias)
    if not qli_field_map:
        blocking_reasons.append(
            "DD_QUOTE_LINE_ITEM_DESCRIBE_UNAVAILABLE: QuoteLineItem describe returned no usable fields."
        )
    else:
        for required_field in ("QuoteId", "PricebookEntryId", "Quantity", "UnitPrice"):
            if not _field_available(qli_field_map, required_field, require_createable=True):
                blocking_reasons.append(
                    f"DD_QUOTE_LINE_ITEM_FIELD_UNAVAILABLE: QuoteLineItem.{required_field} is not createable."
                )
        if any(line.discount_pct is not None for line in persisted_plan.line_items) and not _field_available(
            qli_field_map,
            "Discount",
            require_createable=True,
        ):
            blocking_reasons.append(
                "DD_QUOTE_LINE_ITEM_DISCOUNT_UNAVAILABLE: QuoteLineItem.Discount is not createable for requested line discounts."
            )

    existing_quote_line_items = _find_existing_line_items(quote_id=quote_id, org_alias=org_alias)
    resolutions, resolution_blockers = _resolve_pricebook_entries(
        pricebook_id=pricebook_id,
        line_items=persisted_plan.line_items,
        org_alias=org_alias,
    )
    blocking_reasons.extend(resolution_blockers)

    existing_by_pricebook_entry = {
        str(row.get("PricebookEntryId") or "").strip(): row
        for row in existing_quote_line_items
        if str(row.get("PricebookEntryId") or "").strip()
    }
    quote_line_item_values: list[dict[str, Any]] = []
    skipped_line_items: list[dict[str, Any]] = []
    created_line_item_ids: list[str] = []
    created_quote_line_items: list[dict[str, Any]] = []

    if not blocking_reasons:
        for resolution in resolutions:
            existing = existing_by_pricebook_entry.get(resolution["pricebook_entry_id"])
            if existing is not None:
                skipped_line_items.append(
                    {
                        "line_index": resolution["line_index"],
                        "pricebook_entry_id": resolution["pricebook_entry_id"],
                        "existing_quote_line_item_id": existing.get("Id"),
                        "reason": "pricebook entry already exists on quote",
                    }
                )
                continue
            if _coerce_float(resolution.get("unit_price")) is None and _coerce_float(
                resolution.get("default_unit_price")
            ) is None:
                blocking_reasons.append(
                    "DD_MISSING_UNIT_PRICE: "
                    f"Line {resolution['line_index']} did not provide unit_price and the resolved "
                    "PricebookEntry does not have UnitPrice populated."
                )
                continue
            values = _build_quote_line_item_values(
                quote_id=quote_id,
                resolution=resolution,
                qli_field_map=qli_field_map,
            )
            missing_fields = _missing_required_fields(qli_field_map, values)
            if missing_fields:
                blocking_reasons.append(
                    "DD_MISSING_REQUIRED_QUOTE_LINE_ITEM_FIELDS: "
                    f"Line {resolution['line_index']} is missing required QuoteLineItem fields: "
                    + ", ".join(missing_fields)
                    + "."
                )
                continue
            quote_line_item_values.append(values)

    if blocking_reasons:
        return _render_line_item_result(
            quote_id=quote_id,
            opportunity_id=opportunity_id,
            quote_plan=persisted_plan,
            quote_plan_path=str(quote_plan_path),
            readiness_after=readiness_after,
            planning=planning,
            status="blocked",
            created=False,
            blocking_reasons=_dedupe_strings(blocking_reasons),
            resolutions=resolutions,
            quote_line_item_values=quote_line_item_values,
            existing_quote_line_items=existing_quote_line_items,
            created_line_item_ids=[],
            created_quote_line_items=[],
            skipped_line_items=skipped_line_items,
        )

    try:
        for values in quote_line_item_values:
            result = sfdc_helpers.create_record("QuoteLineItem", values, org_alias=org_alias)
            line_item_id = _record_id_from_result(result)
            if not line_item_id:
                raise RuntimeError("QuoteLineItem create did not return a Salesforce QuoteLineItem ID.")
            created_line_item_ids.append(line_item_id)
    except Exception as exc:
        postcheck_line_items = _find_existing_line_items(quote_id=quote_id, org_alias=org_alias)
        created_quote_line_items = [
            row
            for row in postcheck_line_items
            if str(row.get("Id") or "").strip() in set(created_line_item_ids)
        ]
        return _render_line_item_result(
            quote_id=quote_id,
            opportunity_id=opportunity_id,
            quote_plan=persisted_plan,
            quote_plan_path=str(quote_plan_path),
            readiness_after=readiness_after,
            planning=planning,
            status="partial_failure",
            created=bool(created_line_item_ids),
            blocking_reasons=[
                "DD_QUOTE_LINE_ITEM_CREATE_FAILED: "
                f"QuoteLineItem create failed after {len(created_line_item_ids)} successful inserts. "
                f"Details: {exc}"
            ],
            resolutions=resolutions,
            quote_line_item_values=quote_line_item_values,
            existing_quote_line_items=postcheck_line_items,
            created_line_item_ids=created_line_item_ids,
            created_quote_line_items=created_quote_line_items,
            skipped_line_items=skipped_line_items,
        )

    postcheck_line_items = _find_existing_line_items(quote_id=quote_id, org_alias=org_alias)
    created_quote_line_items = [
        row for row in postcheck_line_items if str(row.get("Id") or "").strip() in set(created_line_item_ids)
    ]
    if len(created_quote_line_items) != len(created_line_item_ids):
        return _render_line_item_result(
            quote_id=quote_id,
            opportunity_id=opportunity_id,
            quote_plan=persisted_plan,
            quote_plan_path=str(quote_plan_path),
            readiness_after=readiness_after,
            planning=planning,
            status="partial_failure",
            created=bool(created_line_item_ids),
            blocking_reasons=[
                "DD_QUOTE_LINE_ITEM_POSTCHECK_FAILED: "
                f"QuoteLineItem postcheck failed for quote {quote_id}: not all created line items reloaded successfully."
            ],
            resolutions=resolutions,
            quote_line_item_values=quote_line_item_values,
            existing_quote_line_items=postcheck_line_items,
            created_line_item_ids=created_line_item_ids,
            created_quote_line_items=created_quote_line_items,
            skipped_line_items=skipped_line_items,
        )

    observed_at = utc_now_iso()
    run_id = str(args.get("run_id") or args.get("action_details", {}).get("run_id") or "").strip() or None
    allow_insufficient_evidence = bool(args.get("allow_insufficient_evidence"))
    event = build_pattern_update_event(
        source_type="line_items_receipt",
        crm_org_id=crm_org_id,
        opportunity_id=opportunity_id,
        source_ref=f"dealdesk.quote.line_items.upsert:{quote_id}",
        observed_at=observed_at,
        source_receipt_id=run_id,
        raw_record={**quote, "QuoteId": quote_id, "QuoteLineItemCount": len(postcheck_line_items)},
        extracted_motion_id=context.inferred_motion_id,
        confidence=float(persisted_plan.confidence),
        payment_terms=persisted_plan.requested_payment_terms,
        template_key=persisted_plan.requested_template_key,
        discount_pct=persisted_plan.requested_discount_pct,
        outcome="upserted",
        custom={
            "quote_id": quote_id,
            "header_only": False,
            "created_line_item_ids": list(created_line_item_ids),
            "created_count": len(created_line_item_ids),
            "skipped_count": len(skipped_line_items),
            "allow_insufficient_evidence": allow_insufficient_evidence,
        },
        evidence_ids=[*persisted_plan.evidence_ids, opportunity_id, quote_id, *created_line_item_ids],
    )
    event_paths = save_pattern_update_events(
        store=store,
        events=[event],
        org_alias=org_alias,
        refresh_profile=True,
    )
    context_path = update_context_for_line_items(
        store=store,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        opportunity_id=opportunity_id,
        quote_id=quote_id,
        observed_at=observed_at,
        receipt_id=run_id,
        account_id=context.account_id,
    )
    return _render_line_item_result(
        quote_id=quote_id,
        opportunity_id=opportunity_id,
        quote_plan=persisted_plan,
        quote_plan_path=str(quote_plan_path),
        readiness_after=readiness_after,
        planning=planning,
        status="upserted",
        created=bool(created_line_item_ids),
        blocking_reasons=[],
        resolutions=resolutions,
        quote_line_item_values=quote_line_item_values,
        existing_quote_line_items=postcheck_line_items,
        created_line_item_ids=created_line_item_ids,
        created_quote_line_items=created_quote_line_items,
        context_path=str(context_path),
        pattern_event_paths=[str(path) for path in event_paths],
        skipped_line_items=skipped_line_items,
    )


def handle_dealdesk_quote_rollback(
    step: PlanStep,
    tool_def: AutopilotToolDefinition | None,
) -> dict[str, object]:
    del tool_def
    args = step.args or {}
    quote_id = _resolve_quote_id(args)
    if not quote_id:
        raise RuntimeError("Missing quote_id for dealdesk quote rollback.")

    org_alias = str(args.get("org_alias") or "").strip() or None
    crm_org_id = resolve_crm_org_id(org_alias=org_alias, crm_org_id=args.get("crm_org_id"))
    store = DealDeskStore(args.get("state_dir"))

    quote = _load_created_quote(quote_id=quote_id, org_alias=org_alias)
    opportunity_id = _resolve_opportunity_id(args) or str(quote.get("OpportunityId") or "").strip() or None
    if not opportunity_id:
        return _render_rollback_result(
            quote_id=quote_id,
            opportunity_id=None,
            status="blocked",
            blocking_reasons=[
                "DD_ROLLBACK_OPPORTUNITY_UNRESOLVED: Provide opportunity_id when the Quote cannot be loaded."
            ],
            requested_rollback_data=_rollback_data(quote_id),
            remaining_rollback_data=_rollback_data(quote_id),
            quote=quote,
        )

    requested_rollback_data = args.get("rollback_data") if isinstance(args.get("rollback_data"), dict) else {}
    resolved_rollback = _resolve_rollback_targets(
        quote_id=quote_id,
        requested_rollback_data=requested_rollback_data,
        org_alias=org_alias,
    )
    rollback_blockers = list(resolved_rollback["blocking_reasons"])
    rollback_data = resolved_rollback["rollback_data"]

    context = store.load_context(
        opportunity_id=opportunity_id,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )

    process_instances: list[dict[str, Any]] = []
    try:
        process_instances = _find_process_instances(quote_id=quote_id, org_alias=org_alias)
    except Exception as exc:
        rollback_blockers.append(
            "DD_ROLLBACK_APPROVAL_BOUNDARY_UNVERIFIED: "
            f"Quote {quote_id} approval workflow state could not be verified. Details: {exc}"
        )
    if process_instances:
        rollback_blockers.append(
            f"DD_ROLLBACK_PROCESS_INSTANCE: Quote {quote_id} already has approval workflow history; automatic rollback is refused."
        )

    quote_status = str(quote.get("Status") or "").strip() if quote else ""
    if quote_status == "Accepted":
        rollback_blockers.append(
            f"DD_ROLLBACK_QUOTE_ACCEPTED: Quote {quote_id} is already Accepted; automatic rollback is refused."
        )

    if rollback_blockers:
        return _render_rollback_result(
            quote_id=quote_id,
            opportunity_id=opportunity_id,
            status="manual_compensation_required" if process_instances or quote_status == "Accepted" else "blocked",
            blocking_reasons=_dedupe_strings(rollback_blockers),
            requested_rollback_data=rollback_data,
            remaining_rollback_data=rollback_data,
            quote=quote,
            process_instances=process_instances,
        )

    deleted_records: list[dict[str, Any]] = []
    skipped_missing_records: list[dict[str, Any]] = []
    failure_reasons: list[str] = []
    stop_deletion = False
    for sobject in rollback_data.get("delete_order", []):
        if stop_deletion:
            break
        record_ids = rollback_data.get("records_by_type", {}).get(sobject, [])
        for record_id in record_ids:
            if not _record_exists_for_rollback(sobject=sobject, record_id=record_id, org_alias=org_alias):
                skipped_missing_records.append(
                    {
                        "sobject": sobject,
                        "record_id": record_id,
                        "reason": "record already missing",
                    }
                )
                continue
            try:
                result = sfdc_helpers.delete_record(sobject, record_id, org_alias=org_alias)
            except Exception as exc:
                failure_reasons.append(
                    f"DD_ROLLBACK_DELETE_FAILED: {sobject} {record_id} delete failed. Details: {exc}"
                )
                stop_deletion = True
                break
            deleted_records.append(
                {
                    "sobject": sobject,
                    "record_id": record_id,
                    "result": result,
                }
            )

    remaining_records_by_type = _remaining_rollback_records(
        rollback_data=rollback_data,
        org_alias=org_alias,
    )
    verification_failures = [
        f"DD_ROLLBACK_POSTCHECK_FAILED: {sobject} {record_id} still exists after rollback delete."
        for sobject, record_ids in remaining_records_by_type.items()
        for record_id in record_ids
        if any(
            deleted["sobject"] == sobject and deleted["record_id"] == record_id
            for deleted in deleted_records
        )
    ]
    failure_reasons.extend(verification_failures)

    remaining_quote_ids = list(remaining_records_by_type.get("Quote") or [])
    remaining_line_item_ids = list(remaining_records_by_type.get("QuoteLineItem") or [])
    remaining_rollback_data = _rollback_data(
        remaining_quote_ids[0] if remaining_quote_ids else None,
        line_item_ids=remaining_line_item_ids,
    )
    status = "rolled_back" if not remaining_quote_ids and not remaining_line_item_ids and not failure_reasons else "partial_failure"

    pattern_event_paths: list[str] = []
    context_path: str | None = None
    if status == "rolled_back" and deleted_records:
        observed_at = utc_now_iso()
        run_id = str(args.get("run_id") or args.get("action_details", {}).get("run_id") or "").strip() or None
        event = build_pattern_update_event(
            source_type="rollback_receipt",
            crm_org_id=crm_org_id,
            opportunity_id=opportunity_id,
            source_ref=f"dealdesk.quote.rollback:{quote_id}",
            observed_at=observed_at,
            source_receipt_id=run_id,
            raw_record=quote or {"Id": quote_id, "OpportunityId": opportunity_id},
            outcome="rolled_back",
            custom={
                "quote_id": quote_id,
                "deleted_records": [dict(record) for record in deleted_records],
                "skipped_missing_records": list(skipped_missing_records),
            },
            evidence_ids=[
                opportunity_id,
                quote_id,
                *[str(record["record_id"]) for record in deleted_records],
            ],
        )
        event_paths = save_pattern_update_events(
            store=store,
            events=[event],
            org_alias=org_alias,
            refresh_profile=True,
        )
        pattern_event_paths = [str(path) for path in event_paths]
        context_path = str(
            update_context_for_quote_rollback(
                store=store,
                crm_org_id=crm_org_id,
                org_alias=org_alias,
                opportunity_id=opportunity_id,
                quote_id=quote_id,
                observed_at=observed_at,
                receipt_id=run_id,
                account_id=context.account_id if context is not None else None,
            )
        )

    return _render_rollback_result(
        quote_id=quote_id,
        opportunity_id=opportunity_id,
        status=status,
        blocking_reasons=_dedupe_strings(failure_reasons),
        requested_rollback_data=rollback_data,
        remaining_rollback_data=remaining_rollback_data,
        quote=quote,
        deleted_records=deleted_records,
        skipped_missing_records=skipped_missing_records,
        process_instances=process_instances,
        context_path=context_path,
        pattern_event_paths=pattern_event_paths,
    )


def _build_planning_state(
    step: PlanStep,
    *,
    quote_request: dict[str, Any] | None = None,
    persist_plan: bool,
) -> dict[str, Any]:
    args = dict(step.args or {})
    opportunity_id = _resolve_opportunity_id(args)
    if not opportunity_id:
        raise RuntimeError("Missing opportunity_id for dealdesk quote planning.")

    planning_step = _step_with_quote_request(step, quote_request) if quote_request is not None else step
    readiness_result = handle_dealdesk_quote_readiness(planning_step, None)
    readiness_after = readiness_result.get("after") or {}
    readiness = QuoteReadinessResult.model_validate(readiness_after.get("readiness") or {})

    org_alias = str(args.get("org_alias") or "").strip() or None
    crm_org_id = resolve_crm_org_id(org_alias=org_alias, crm_org_id=args.get("crm_org_id"))
    store = DealDeskStore(args.get("state_dir"))
    context = store.load_context(
        opportunity_id=opportunity_id,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )
    if context is None:
        raise RuntimeError("Quote planning could not load the DealContext produced by readiness.")
    environment_profile = store.load_quote_environment_profile(crm_org_id=crm_org_id, org_alias=org_alias)
    if environment_profile is None:
        raise RuntimeError("Quote planning could not load the QuoteEnvironmentProfile produced by readiness.")

    generated_at = datetime.now(timezone.utc).isoformat()
    planning = build_quote_draft_plan(
        crm_org_id=crm_org_id,
        opportunity_id=opportunity_id,
        environment_profile=environment_profile,
        readiness=readiness,
        context=context,
        quote_request=quote_request if quote_request is not None else args.get("quote_request"),
        generated_at=generated_at,
        allow_insufficient_evidence=bool(args.get("allow_insufficient_evidence")),
    )
    quote_plan_path = store.quote_draft_plan_path(
        opportunity_id=opportunity_id,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )
    if persist_plan:
        quote_plan_path = store.save_quote_draft_plan(
            planning.quote_plan,
            crm_org_id=crm_org_id,
            org_alias=org_alias,
        )
    return {
        "targets": readiness_result.get("targets") or [opportunity_id],
        "before": readiness_result.get("before") or {"opportunity_id": opportunity_id},
        "readiness_after": readiness_after,
        "planning": planning,
        "quote_plan_path": str(quote_plan_path),
        "context": context,
        "environment_profile": environment_profile,
    }


def _render_planning_result(
    *,
    planning: QuoteDraftPlanResult,
    readiness_after: dict[str, object],
    quote_plan_path: str,
) -> dict[str, object]:
    return {
        "status": "execution_eligible" if planning.execution_eligible else "blocked",
        "execution_eligible": planning.execution_eligible,
        "blocking_reasons": list(planning.blocking_reasons),
        "quote_plan": planning.quote_plan.to_dict(),
        "quote_plan_path": quote_plan_path,
        "readiness": readiness_after.get("readiness"),
        "readiness_mode": planning.readiness_mode,
        "context_id": readiness_after.get("context_id"),
        "context_path": readiness_after.get("context_path"),
        "environment_profile_id": readiness_after.get("environment_profile_id"),
        "environment_profile_path": readiness_after.get("environment_profile_path"),
        "pattern_profile_id": readiness_after.get("pattern_profile_id"),
        "pattern_profile_path": readiness_after.get("pattern_profile_path"),
        "config_draft_path": readiness_after.get("config_draft_path"),
    }


def _render_create_result(
    *,
    opportunity_id: str,
    quote_plan_path: str,
    quote_plan: QuoteDraftPlanV1,
    readiness_after: dict[str, object],
    planning: QuoteDraftPlanResult,
    status: str,
    created: bool,
    blocking_reasons: list[str],
    quote_values: dict[str, Any],
    pricebook_id: str | None,
    pricebook_source: str | None,
    duplicate_quotes: list[dict[str, Any]],
    quote_id: str | None = None,
    quote: dict[str, Any] | None = None,
    result: dict[str, Any] | None = None,
    context_path: str | None = None,
    pattern_event_paths: list[str] | None = None,
) -> dict[str, object]:
    return {
        "targets": [quote_id] if quote_id else [opportunity_id],
        "before": {
            "opportunity_id": opportunity_id,
            "quote_request_id": quote_plan.quote_request_id,
            "quote_plan_path": quote_plan_path,
        },
        "after": {
            "status": status,
            "created": created,
            "blocking_reasons": list(blocking_reasons),
            "quote_plan": quote_plan.to_dict(),
            "quote_plan_path": quote_plan_path,
            "approval_required": quote_plan.approval_required,
            "approval_reasoning": list(quote_plan.approval_reasoning),
            "readiness": readiness_after.get("readiness"),
            "readiness_mode": planning.readiness_mode,
            "pricebook_id": pricebook_id,
            "pricebook_source": pricebook_source,
            "duplicate_active_quotes": duplicate_quotes,
            "quote_values": quote_values,
            "quote_id": quote_id,
            "quote": quote or {},
            "result": result or {},
            "context_path": context_path or readiness_after.get("context_path"),
            "environment_profile_path": readiness_after.get("environment_profile_path"),
            "pattern_profile_path": readiness_after.get("pattern_profile_path"),
            "pattern_event_paths": list(pattern_event_paths or []),
            "rollback_data": _rollback_data(quote_id),
        },
    }


def _render_line_item_result(
    *,
    quote_id: str,
    opportunity_id: str | None,
    quote_plan: QuoteDraftPlanV1 | None,
    quote_plan_path: str | None,
    readiness_after: dict[str, object],
    planning: QuoteDraftPlanResult | None,
    status: str,
    created: bool,
    blocking_reasons: list[str],
    resolutions: list[dict[str, Any]],
    quote_line_item_values: list[dict[str, Any]],
    existing_quote_line_items: list[dict[str, Any]],
    created_line_item_ids: list[str],
    created_quote_line_items: list[dict[str, Any]],
    context_path: str | None = None,
    pattern_event_paths: list[str] | None = None,
    skipped_line_items: list[dict[str, Any]] | None = None,
) -> dict[str, object]:
    return {
        "targets": list(created_line_item_ids or [quote_id]),
        "before": {
            "quote_id": quote_id,
            "opportunity_id": opportunity_id,
            "quote_plan_path": quote_plan_path,
            "quote_request_id": quote_plan.quote_request_id if quote_plan is not None else None,
        },
        "after": {
            "status": status,
            "created": created,
            "blocking_reasons": list(blocking_reasons),
            "quote_plan": quote_plan.to_dict() if quote_plan is not None else {},
            "quote_plan_path": quote_plan_path,
            "readiness": readiness_after.get("readiness"),
            "readiness_mode": planning.readiness_mode if planning is not None else None,
            "quote_id": quote_id,
            "opportunity_id": opportunity_id,
            "resolutions": list(resolutions),
            "quote_line_item_values": list(quote_line_item_values),
            "existing_quote_line_items": list(existing_quote_line_items),
            "created_line_item_ids": list(created_line_item_ids),
            "created_quote_line_items": list(created_quote_line_items),
            "skipped_line_items": list(skipped_line_items or []),
            "context_path": context_path or readiness_after.get("context_path"),
            "environment_profile_path": readiness_after.get("environment_profile_path"),
            "pattern_profile_path": readiness_after.get("pattern_profile_path"),
            "pattern_event_paths": list(pattern_event_paths or []),
            "rollback_data": _rollback_data(quote_id, line_item_ids=created_line_item_ids),
        },
    }


def _render_rollback_result(
    *,
    quote_id: str,
    opportunity_id: str | None,
    status: str,
    blocking_reasons: list[str],
    requested_rollback_data: dict[str, Any],
    remaining_rollback_data: dict[str, Any],
    quote: dict[str, Any] | None = None,
    deleted_records: list[dict[str, Any]] | None = None,
    skipped_missing_records: list[dict[str, Any]] | None = None,
    process_instances: list[dict[str, Any]] | None = None,
    context_path: str | None = None,
    pattern_event_paths: list[str] | None = None,
) -> dict[str, object]:
    return {
        "targets": [quote_id],
        "before": {
            "quote_id": quote_id,
            "opportunity_id": opportunity_id,
            "rollback_data": requested_rollback_data,
        },
        "after": {
            "status": status,
            "quote_id": quote_id,
            "opportunity_id": opportunity_id,
            "quote": quote or {},
            "blocking_reasons": list(blocking_reasons),
            "deleted_records": list(deleted_records or []),
            "skipped_missing_records": list(skipped_missing_records or []),
            "process_instances": list(process_instances or []),
            "rollback_data": requested_rollback_data,
            "remaining_rollback_data": remaining_rollback_data,
            "context_path": context_path,
            "pattern_event_paths": list(pattern_event_paths or []),
        },
    }


def _quote_request_from_plan(plan: QuoteDraftPlanV1) -> dict[str, Any]:
    payload: dict[str, Any] = {
        "line_items": [_line_item_payload(line_item) for line_item in plan.line_items],
    }
    if plan.requested_template_key not in (None, ""):
        payload["template_key"] = plan.requested_template_key
    if plan.requested_payment_terms not in (None, ""):
        payload["payment_terms"] = plan.requested_payment_terms
    if plan.requested_discount_pct is not None:
        payload["discount_pct"] = plan.requested_discount_pct
    return payload


def _line_item_payload(line_item: QuoteLineDraft) -> dict[str, Any]:
    payload = line_item.to_dict()
    payload.pop("$schema", None)
    return {key: value for key, value in payload.items() if value is not None}


def _step_with_quote_request(step: PlanStep, quote_request: dict[str, Any]) -> PlanStep:
    args = dict(step.args or {})
    args["quote_request"] = dict(quote_request)
    return PlanStep(
        step_id=step.step_id,
        tool=step.tool,
        diff_preview=step.diff_preview,
        args=args,
        risk_level=step.risk_level,
        idempotency_key=step.idempotency_key,
        prechecks=list(step.prechecks),
        postchecks=list(step.postchecks),
        evidence_ids=list(step.evidence_ids),
        depends_on=list(step.depends_on),
        rollback_on_failure=step.rollback_on_failure,
    )


def _step_with_opportunity_id(step: PlanStep, opportunity_id: str) -> PlanStep:
    args = dict(step.args or {})
    args["opportunity_id"] = opportunity_id
    return PlanStep(
        step_id=step.step_id,
        tool=step.tool,
        diff_preview=step.diff_preview,
        args=args,
        risk_level=step.risk_level,
        idempotency_key=step.idempotency_key,
        prechecks=list(step.prechecks),
        postchecks=list(step.postchecks),
        evidence_ids=list(step.evidence_ids),
        depends_on=list(step.depends_on),
        rollback_on_failure=step.rollback_on_failure,
    )


def _resolve_opportunity_id(args: dict[str, Any]) -> str:
    return str(
        args.get("opportunity_id") or args.get("record_id") or args.get("opportunityId") or ""
    ).strip()


def _resolve_quote_id(args: dict[str, Any]) -> str:
    return str(args.get("quote_id") or args.get("record_id") or args.get("quoteId") or "").strip()


def _describe_field_map(sobject: str, *, org_alias: str | None) -> dict[str, dict[str, Any]]:
    payload = sfdc_helpers.describe_sobject(sobject, org_alias=org_alias)
    fields = payload.get("result", {}).get("fields", []) if isinstance(payload, dict) else []
    mapping: dict[str, dict[str, Any]] = {}
    for field in fields or []:
        if not isinstance(field, dict):
            continue
        name = str(field.get("name") or "").strip()
        if not name:
            continue
        mapping[name.lower()] = field
    return mapping


def _field_available(
    field_map: dict[str, dict[str, Any]],
    field_name: str | None,
    *,
    require_createable: bool = False,
) -> bool:
    if not field_name:
        return False
    info = field_map.get(str(field_name).lower())
    if not info:
        return False
    if info.get("accessible") is False:
        return False
    if require_createable and not info.get("createable", True):
        return False
    return True


def _best_mapping_for_semantic(
    candidates: list[FieldMappingCandidate],
    semantic_key: str,
    *,
    preferred_objects: tuple[str, ...] | None = None,
) -> FieldMappingCandidate | None:
    matches = [candidate for candidate in candidates if candidate.semantic_key == semantic_key]
    if not matches:
        return None
    if preferred_objects:
        for object_name in preferred_objects:
            scoped = [
                candidate
                for candidate in matches
                if candidate.object_name.lower() == object_name.lower()
            ]
            if scoped:
                return max(scoped, key=lambda candidate: float(candidate.confidence))
    return max(matches, key=lambda candidate: float(candidate.confidence))


def _mapped_payload_value(payload: dict[str, Any], mapping: FieldMappingCandidate | None) -> Any:
    if mapping is None:
        return None
    if mapping.object_name.lower() == "account":
        return payload.get(f"Account.{mapping.field_name}")
    return payload.get(mapping.field_name)


def _resolve_pricebook_id(
    *,
    opportunity_snapshot: dict[str, Any],
    environment_profile: QuoteEnvironmentProfileV1,
    org_alias: str | None,
) -> tuple[str | None, str | None]:
    direct_value = str(opportunity_snapshot.get("Pricebook2Id") or "").strip() or None
    if direct_value:
        return direct_value, "opportunity.Pricebook2Id"

    source_mapping = _best_mapping_for_semantic(
        environment_profile.field_mappings,
        "pricebook",
        preferred_objects=("Opportunity", "Quote"),
    )
    mapped_value = _mapped_payload_value(opportunity_snapshot, source_mapping)
    mapped_id = str(mapped_value or "").strip() or None
    if mapped_id:
        return mapped_id, f"{source_mapping.object_name}.{source_mapping.field_name}"

    for signal in environment_profile.pricebook_signals:
        signal_id = str(signal.pricebook_id or "").strip() or None
        if signal.active and signal_id:
            return signal_id, "environment_profile.pricebook_signals"

    rows = sfdc_helpers.run_soql(
        "SELECT Id, Name FROM Pricebook2 WHERE IsStandard = TRUE AND IsActive = TRUE LIMIT 1",
        org_alias=org_alias,
    )
    if rows:
        return str(rows[0].get("Id") or "").strip() or None, "standard_pricebook_query"
    return None, None


def _find_active_quotes(*, opportunity_id: str, org_alias: str | None) -> list[dict[str, Any]]:
    escaped_id = sfdc_helpers.escape_soql_value(opportunity_id)
    rows = sfdc_helpers.run_soql(
        (
            "SELECT Id, Name, Status, CreatedDate "
            f"FROM Quote WHERE OpportunityId = '{escaped_id}' "
            "ORDER BY CreatedDate DESC LIMIT 25"
        ),
        org_alias=org_alias,
    )
    active_rows: list[dict[str, Any]] = []
    for row in rows:
        status = str(row.get("Status") or "").strip()
        if status in _TERMINAL_QUOTE_STATUSES:
            continue
        active_rows.append(row)
    return active_rows


def _build_quote_values(
    *,
    plan: QuoteDraftPlanV1,
    context,
    opportunity_snapshot: dict[str, Any],
    environment_profile: QuoteEnvironmentProfileV1,
    quote_field_map: dict[str, dict[str, Any]],
    pricebook_id: str | None,
) -> dict[str, Any]:
    values: dict[str, Any] = {"OpportunityId": plan.opportunity_id}
    if pricebook_id and _field_available(quote_field_map, "Pricebook2Id", require_createable=True):
        values["Pricebook2Id"] = pricebook_id
    if _field_available(quote_field_map, "Status", require_createable=True):
        values["Status"] = "Draft"

    name_info = quote_field_map.get("name")
    if name_info and _field_available(quote_field_map, "Name", require_createable=True):
        if not name_info.get("autoNumber") and not name_info.get("defaultedOnCreate"):
            values["Name"] = _quote_name(opportunity_snapshot)

    _apply_semantic_quote_value(
        values,
        semantic_key="primary_contact",
        plan=plan,
        context=context,
        opportunity_snapshot=opportunity_snapshot,
        environment_profile=environment_profile,
        quote_field_map=quote_field_map,
        preferred_source_objects=("Opportunity", "Account", "Quote"),
    )
    _apply_semantic_quote_value(
        values,
        semantic_key="legal_entity",
        plan=plan,
        context=context,
        opportunity_snapshot=opportunity_snapshot,
        environment_profile=environment_profile,
        quote_field_map=quote_field_map,
        preferred_source_objects=("Opportunity", "Account", "Quote"),
    )
    _apply_semantic_quote_value(
        values,
        semantic_key="payment_terms",
        plan=plan,
        context=context,
        opportunity_snapshot=opportunity_snapshot,
        environment_profile=environment_profile,
        quote_field_map=quote_field_map,
        preferred_source_objects=("Opportunity", "Quote"),
    )
    _apply_semantic_quote_value(
        values,
        semantic_key="discount_pct",
        plan=plan,
        context=context,
        opportunity_snapshot=opportunity_snapshot,
        environment_profile=environment_profile,
        quote_field_map=quote_field_map,
        preferred_source_objects=("Opportunity", "Quote"),
    )
    _apply_semantic_quote_value(
        values,
        semantic_key="template_key",
        plan=plan,
        context=context,
        opportunity_snapshot=opportunity_snapshot,
        environment_profile=environment_profile,
        quote_field_map=quote_field_map,
        preferred_source_objects=("Quote", "Opportunity"),
    )
    _apply_semantic_quote_value(
        values,
        semantic_key="term_months",
        plan=plan,
        context=context,
        opportunity_snapshot=opportunity_snapshot,
        environment_profile=environment_profile,
        quote_field_map=quote_field_map,
        preferred_source_objects=("Opportunity", "Quote"),
    )
    _apply_semantic_quote_value(
        values,
        semantic_key="contract_start",
        plan=plan,
        context=context,
        opportunity_snapshot=opportunity_snapshot,
        environment_profile=environment_profile,
        quote_field_map=quote_field_map,
        preferred_source_objects=("Opportunity", "Quote"),
    )
    _apply_semantic_quote_value(
        values,
        semantic_key="contract_end",
        plan=plan,
        context=context,
        opportunity_snapshot=opportunity_snapshot,
        environment_profile=environment_profile,
        quote_field_map=quote_field_map,
        preferred_source_objects=("Opportunity", "Quote"),
    )
    return values


def _apply_semantic_quote_value(
    values: dict[str, Any],
    *,
    semantic_key: str,
    plan: QuoteDraftPlanV1,
    context,
    opportunity_snapshot: dict[str, Any],
    environment_profile: QuoteEnvironmentProfileV1,
    quote_field_map: dict[str, dict[str, Any]],
    preferred_source_objects: tuple[str, ...],
) -> None:
    target_mapping = _best_mapping_for_semantic(
        environment_profile.field_mappings,
        semantic_key,
        preferred_objects=("Quote",),
    )
    if target_mapping is None or not _field_available(
        quote_field_map,
        target_mapping.field_name,
        require_createable=True,
    ):
        return
    value = _semantic_source_value(
        semantic_key,
        plan=plan,
        context=context,
        opportunity_snapshot=opportunity_snapshot,
        environment_profile=environment_profile,
        preferred_source_objects=preferred_source_objects,
    )
    if value not in (None, ""):
        values[target_mapping.field_name] = value


def _semantic_source_value(
    semantic_key: str,
    *,
    plan: QuoteDraftPlanV1,
    context,
    opportunity_snapshot: dict[str, Any],
    environment_profile: QuoteEnvironmentProfileV1,
    preferred_source_objects: tuple[str, ...],
) -> Any:
    if semantic_key == "payment_terms" and plan.requested_payment_terms not in (None, ""):
        return plan.requested_payment_terms
    if semantic_key == "discount_pct" and plan.requested_discount_pct is not None:
        return plan.requested_discount_pct
    if semantic_key == "template_key" and plan.requested_template_key not in (None, ""):
        return plan.requested_template_key
    if semantic_key == "term_months":
        line_term = _first_line_term_months(plan.line_items)
        if line_term is not None:
            return line_term
    if context.normalized_quote_request.get(semantic_key) not in (None, ""):
        return context.normalized_quote_request.get(semantic_key)
    source_mapping = _best_mapping_for_semantic(
        environment_profile.field_mappings,
        semantic_key,
        preferred_objects=preferred_source_objects,
    )
    mapped_value = _mapped_payload_value(opportunity_snapshot, source_mapping)
    if mapped_value not in (None, ""):
        return mapped_value
    for assumption in plan.assumptions:
        if assumption.key == semantic_key and assumption.value not in (None, ""):
            return assumption.value
    return None


def _first_line_term_months(line_items: list[QuoteLineDraft]) -> int | None:
    for line_item in line_items:
        if line_item.term_months is not None:
            return int(line_item.term_months)
    return None


def _missing_required_fields(
    field_map: dict[str, dict[str, Any]],
    values: dict[str, Any],
) -> list[str]:
    missing: list[str] = []
    for info in field_map.values():
        name = str(info.get("name") or "").strip()
        if not name:
            continue
        if info.get("accessible") is False:
            continue
        if not info.get("createable", True):
            continue
        if info.get("defaultedOnCreate"):
            continue
        if info.get("nillable", True):
            continue
        if info.get("calculated"):
            continue
        if info.get("autoNumber"):
            continue
        if values.get(name) in (None, ""):
            missing.append(name)
    return sorted(dict.fromkeys(missing))


def _quote_name(opportunity_snapshot: dict[str, Any]) -> str:
    opportunity_name = str(opportunity_snapshot.get("Name") or "Opportunity").strip() or "Opportunity"
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%d")
    return f"{opportunity_name} Quote {timestamp}"


def _record_id_from_result(result: dict[str, Any]) -> str | None:
    if not isinstance(result, dict):
        return None
    inner = result.get("result") if isinstance(result.get("result"), dict) else result
    return str(inner.get("id") or inner.get("recordId") or inner.get("Id") or "").strip() or None


def _load_created_quote(*, quote_id: str, org_alias: str | None) -> dict[str, Any] | None:
    escaped_id = sfdc_helpers.escape_soql_value(quote_id)
    rows = sfdc_helpers.run_soql(
        (
            "SELECT Id, OpportunityId, Pricebook2Id, Status, Name "
            f"FROM Quote WHERE Id = '{escaped_id}' LIMIT 1"
        ),
        org_alias=org_alias,
    )
    return rows[0] if rows else None


def _load_quote_line_item(*, line_item_id: str, org_alias: str | None) -> dict[str, Any] | None:
    escaped_id = sfdc_helpers.escape_soql_value(line_item_id)
    rows = sfdc_helpers.run_soql(
        (
            "SELECT Id, QuoteId, PricebookEntryId, Quantity, UnitPrice, Discount "
            f"FROM QuoteLineItem WHERE Id = '{escaped_id}' LIMIT 1"
        ),
        org_alias=org_alias,
    )
    return rows[0] if rows else None


def _find_existing_line_items(*, quote_id: str, org_alias: str | None) -> list[dict[str, Any]]:
    escaped_id = sfdc_helpers.escape_soql_value(quote_id)
    return list(
        sfdc_helpers.run_soql(
            (
                "SELECT Id, QuoteId, PricebookEntryId, Quantity, UnitPrice, Discount "
                f"FROM QuoteLineItem WHERE QuoteId = '{escaped_id}' "
                "ORDER BY CreatedDate ASC LIMIT 200"
            ),
            org_alias=org_alias,
        )
    )


def _resolve_pricebook_entries(
    *,
    pricebook_id: str | None,
    line_items: list[QuoteLineDraft],
    org_alias: str | None,
) -> tuple[list[dict[str, Any]], list[str]]:
    if not pricebook_id:
        return [], []

    resolutions: list[dict[str, Any]] = []
    blockers: list[str] = []
    for index, line_item in enumerate(line_items, start=1):
        attempts = (
            ("product_code", "Product2.ProductCode", line_item.product_code),
            ("product_id", "Product2Id", line_item.product_id),
            ("product_name", "Product2.Name", line_item.product_name),
        )
        resolved = False
        for attempt_key, field_name, raw_value in attempts:
            value = str(raw_value or "").strip()
            if not value:
                continue
            matches = _query_pricebook_entries(
                pricebook_id=pricebook_id,
                field_name=field_name,
                field_value=value,
                org_alias=org_alias,
            )
            if not matches:
                continue
            if len(matches) > 1:
                blockers.append(
                    f"DD_AMBIGUOUS_PRICEBOOK_ENTRY: line_items[{index}] matched multiple PricebookEntry rows via {attempt_key}={value}."
                )
                resolved = True
                break
            match = matches[0]
            resolutions.append(
                {
                    "line_index": index,
                    "product_reference_type": attempt_key,
                    "product_reference_value": value,
                    "pricebook_entry_id": str(match.get("Id") or "").strip(),
                    "default_unit_price": _coerce_float(match.get("UnitPrice")),
                    "product_id": str(match.get("Product2Id") or line_item.product_id or "").strip() or None,
                    "product_code": str(
                        _related_field_value(match, "Product2", "ProductCode") or line_item.product_code or ""
                    ).strip()
                    or None,
                    "product_name": str(
                        _related_field_value(match, "Product2", "Name") or line_item.product_name or ""
                    ).strip()
                    or None,
                    "quantity": float(line_item.quantity),
                    "unit_price": line_item.unit_price,
                    "discount_pct": line_item.discount_pct,
                    "term_months": line_item.term_months,
                }
            )
            resolved = True
            break
        if resolved:
            continue
        blockers.append(
            f"DD_PRICEBOOK_ENTRY_NOT_FOUND: line_items[{index}] did not resolve to an active PricebookEntry in pricebook {pricebook_id}."
        )
    return resolutions, blockers


def _query_pricebook_entries(
    *,
    pricebook_id: str,
    field_name: str,
    field_value: str,
    org_alias: str | None,
) -> list[dict[str, Any]]:
    escaped_pricebook_id = sfdc_helpers.escape_soql_value(pricebook_id)
    escaped_field_value = sfdc_helpers.escape_soql_value(field_value)
    query = (
        "SELECT Id, UnitPrice, Product2Id, Product2.Name, Product2.ProductCode "
        "FROM PricebookEntry "
        f"WHERE Pricebook2Id = '{escaped_pricebook_id}' "
        "AND IsActive = TRUE "
        f"AND {field_name} = '{escaped_field_value}' "
        "LIMIT 5"
    )
    return list(sfdc_helpers.run_soql(query, org_alias=org_alias))


def _build_quote_line_item_values(
    *,
    quote_id: str,
    resolution: dict[str, Any],
    qli_field_map: dict[str, dict[str, Any]],
) -> dict[str, Any]:
    unit_price = _coerce_float(resolution.get("unit_price"))
    if unit_price is None:
        unit_price = _coerce_float(resolution.get("default_unit_price"))
    values: dict[str, Any] = {
        "QuoteId": quote_id,
        "PricebookEntryId": resolution["pricebook_entry_id"],
        "Quantity": float(resolution["quantity"]),
    }
    product_id = str(resolution.get("product_id") or "").strip() or None
    if product_id and _field_available(qli_field_map, "Product2Id", require_createable=True):
        values["Product2Id"] = product_id
    if unit_price is not None and _field_available(qli_field_map, "UnitPrice", require_createable=True):
        values["UnitPrice"] = unit_price
    if resolution.get("discount_pct") is not None and _field_available(
        qli_field_map,
        "Discount",
        require_createable=True,
    ):
        values["Discount"] = float(resolution["discount_pct"])
    return values


def _related_field_value(payload: dict[str, Any], relationship: str, field_name: str) -> Any:
    relationship_payload = payload.get(relationship)
    if isinstance(relationship_payload, dict) and relationship_payload.get(field_name) not in (None, ""):
        return relationship_payload.get(field_name)
    dotted_key = f"{relationship}.{field_name}"
    return payload.get(dotted_key)


def _coerce_float(value: Any) -> float | None:
    if value in (None, ""):
        return None
    try:
        return float(value)
    except Exception:
        return None


def _find_process_instances(*, quote_id: str, org_alias: str | None) -> list[dict[str, Any]]:
    escaped_id = sfdc_helpers.escape_soql_value(quote_id)
    return list(
        sfdc_helpers.run_soql(
            (
                "SELECT Id, Status, CreatedDate "
                f"FROM ProcessInstance WHERE TargetObjectId = '{escaped_id}' "
                "ORDER BY CreatedDate DESC LIMIT 20"
            ),
            org_alias=org_alias,
        )
    )


def _record_exists_for_rollback(*, sobject: str, record_id: str, org_alias: str | None) -> bool:
    if sobject == "Quote":
        return _load_created_quote(quote_id=record_id, org_alias=org_alias) is not None
    if sobject == "QuoteLineItem":
        return _load_quote_line_item(line_item_id=record_id, org_alias=org_alias) is not None
    raise RuntimeError(f"Unsupported rollback sobject: {sobject}")


def _resolve_rollback_targets(
    *,
    quote_id: str,
    requested_rollback_data: dict[str, Any],
    org_alias: str | None,
) -> dict[str, Any]:
    records_by_type = (
        requested_rollback_data.get("records_by_type")
        if isinstance(requested_rollback_data.get("records_by_type"), dict)
        else {}
    )
    requested_quote_ids = _normalize_record_ids(records_by_type.get("Quote"))
    requested_line_item_ids = _normalize_record_ids(records_by_type.get("QuoteLineItem"))

    blocking_reasons: list[str] = []
    if requested_quote_ids and quote_id not in requested_quote_ids:
        blocking_reasons.append(
            "DD_ROLLBACK_QUOTE_MISMATCH: rollback_data.Quote does not contain the requested quote_id."
        )

    effective_quote_id = requested_quote_ids[0] if requested_quote_ids else quote_id
    if not requested_line_item_ids:
        requested_line_item_ids = [
            str(row.get("Id") or "").strip()
            for row in _find_existing_line_items(quote_id=quote_id, org_alias=org_alias)
            if str(row.get("Id") or "").strip()
        ]

    rollback_data = _rollback_data(effective_quote_id, line_item_ids=requested_line_item_ids)
    delete_order = requested_rollback_data.get("delete_order")
    if isinstance(delete_order, list):
        filtered_delete_order = [
            str(item)
            for item in delete_order
            if str(item) in {"QuoteLineItem", "Quote"}
        ]
        if filtered_delete_order:
            rollback_data["delete_order"] = filtered_delete_order
    return {
        "rollback_data": rollback_data,
        "blocking_reasons": blocking_reasons,
    }


def _remaining_rollback_records(
    *,
    rollback_data: dict[str, Any],
    org_alias: str | None,
) -> dict[str, list[str]]:
    remaining: dict[str, list[str]] = {}
    records_by_type = rollback_data.get("records_by_type", {})
    if not isinstance(records_by_type, dict):
        return {"QuoteLineItem": [], "Quote": []}
    for sobject in ("QuoteLineItem", "Quote"):
        remaining[sobject] = [
            record_id
            for record_id in _normalize_record_ids(records_by_type.get(sobject))
            if _record_exists_for_rollback(sobject=sobject, record_id=record_id, org_alias=org_alias)
        ]
    return remaining


def _rollback_data(
    quote_id: str | None,
    *,
    line_item_ids: list[str] | None = None,
) -> dict[str, Any]:
    ordered_line_item_ids = [item_id for item_id in (line_item_ids or []) if item_id]
    quote_ids = [quote_id] if quote_id else []
    return {
        "record_ids": [*ordered_line_item_ids, *quote_ids],
        "sobject": "QuoteLineItem" if ordered_line_item_ids else "Quote",
        "records_by_type": {
            "QuoteLineItem": ordered_line_item_ids,
            "Quote": quote_ids,
        },
        "automatic_delete_eligible": bool(quote_id),
        "delete_order": ["QuoteLineItem", "Quote"] if ordered_line_item_ids else ["Quote"],
        "rollback_eligible_until": "first approval action or quote acceptance",
        "instructions": (
            "Delete created QuoteLineItem records first, then delete the Quote, "
            "before any approval workflow side effects or quote acceptance exist."
        ),
    }


def _normalize_record_ids(values: Any) -> list[str]:
    if not isinstance(values, list):
        return []
    return _dedupe_strings([str(value or "").strip() for value in values if str(value or "").strip()])


def _dedupe_strings(values: list[str]) -> list[str]:
    seen: set[str] = set()
    ordered: list[str] = []
    for value in values:
        text = str(value or "").strip()
        if not text or text in seen:
            continue
        ordered.append(text)
        seen.add(text)
    return ordered


__all__ = [
    "handle_dealdesk_quote_create",
    "handle_dealdesk_quote_line_items_upsert",
    "handle_dealdesk_quote_rollback",
    "handle_dealdesk_quote_plan",
]
