"""Core checks blocking ambiguity and compiles the private first-build brief."""

from src.domain.projects.discovery_fidelity import (
    apply_fidelity_hygiene,
    normalize_capability_intent,
    reconcile_requested_capabilities,
    should_promote_ready,
)
from src.domain.projects.discovery_schemas import (
    DiscoveryQuestion,
    DiscoveryReadiness,
    DiscoveryResult,
    DiscoverySpec,
    DiscoveryTurnResult,
    OpenDecision,
)

CONTENT_ARCHETYPES = {"marketing_site", "landing_page", "content_platform"}
TENANCY_ARCHETYPES = {"saas", "marketplace", "client_portal"}
MARKETPLACE_ARCHETYPES = {"marketplace"}
ACCESS_HINTS = ("access", "permission", "see", "own", "isolat", "tenant", "workspace", "assign")

# Core-computed, deterministic gaps from validate_discovery_readiness. These ids are a function
# of the spec, not of the model's own free-text phrasing, so two turns blocked by the same
# grounded gap should expose the same decision_id even when the model asked about it differently.
DETERMINISTIC_BLOCKER_IDS = frozenset(
    {
        "missing_product_purpose",
        "missing_first_build_goal",
        "missing_primary_workflow",
        "missing_actor_model",
        "unresolved_tenancy",
        "unresolved_access_boundary",
        "unresolved_billing_owner",
        "unresolved_marketplace_relationship",
        "unresolved_required_integration",
    }
)


def is_content_site(spec: DiscoverySpec) -> bool:
    archetypes = {item.lower() for item in spec.archetypes}
    return bool(archetypes) and archetypes <= CONTENT_ARCHETYPES


def requires_actor_model(spec: DiscoverySpec) -> bool:
    return not is_content_site(spec)


def requires_tenancy_resolution(spec: DiscoverySpec) -> bool:
    archetypes = {item.lower() for item in spec.archetypes}
    if archetypes & TENANCY_ARCHETYPES:
        return True
    names = " ".join(actor.name.lower() for actor in spec.actors)
    return "company" in names or "organization" in names or "tenant" in names


def has_material_role_differences(spec: DiscoverySpec) -> bool:
    return len(spec.actors) >= 2


def permissions_are_ambiguous(spec: DiscoverySpec) -> bool:
    if any((actor.access_scope or "").strip() for actor in spec.actors):
        return False
    return not any(
        any(hint in rule.rule.lower() for hint in ACCESS_HINTS) for rule in spec.business_rules
    )


def billing_is_core_scope(spec: DiscoverySpec) -> bool:
    return bool(spec.billing and spec.billing.required)


def billing_ownership_unknown(spec: DiscoverySpec) -> bool:
    billing = spec.billing
    if not billing:
        return True
    return not ((billing.payer or "").strip() or (billing.billing_owner or "").strip())


def marketplace_model(spec: DiscoverySpec) -> bool:
    return bool({item.lower() for item in spec.archetypes} & MARKETPLACE_ARCHETYPES)


def marketplace_relationship_unknown(spec: DiscoverySpec) -> bool:
    if len(spec.actors) >= 2:
        return False
    return not any(
        any(hint in rule.rule.lower() for hint in ("buyer", "seller", "supply", "demand"))
        for rule in spec.business_rules
    )


def primary_workflow_requires_integration(spec: DiscoverySpec) -> bool:
    return any(item.required_for_first_build for item in spec.integrations)


def required_integration_unknown(spec: DiscoverySpec) -> bool:
    return any(
        item.required_for_first_build and not (item.provider or "").strip()
        for item in spec.integrations
    )


def working_product_name(spec: DiscoverySpec) -> str:
    name = (spec.product.name or "").strip()
    if name and spec.product.name_status != "unknown":
        return name[:255]
    source = spec.product.purpose or spec.product.first_build_goal
    words = [
        word
        for word in source.replace("Build", " ").replace("build", " ").split()
        if word.lower() not in {"a", "an", "the", "for", "my"}
    ]
    title = " ".join(words[:5]).strip().title() or "Untitled App"
    return title[:255]


def validate_discovery_readiness(spec: DiscoverySpec) -> list[str]:
    blockers: list[str] = []
    purpose = spec.product.purpose.strip()
    goal = spec.product.first_build_goal.strip()
    if not purpose and not goal:
        blockers.append("missing_product_purpose")
    if not goal and not spec.workflows:
        blockers.append("missing_first_build_goal")
    if not spec.workflows:
        blockers.append("missing_primary_workflow")
    if requires_actor_model(spec) and not spec.actors:
        blockers.append("missing_actor_model")
    if requires_tenancy_resolution(spec):
        if not spec.tenancy or spec.tenancy.model == "unknown":
            blockers.append("unresolved_tenancy")
    if has_material_role_differences(spec) and permissions_are_ambiguous(spec):
        blockers.append("unresolved_access_boundary")
    if billing_is_core_scope(spec) and billing_ownership_unknown(spec):
        blockers.append("unresolved_billing_owner")
    if marketplace_model(spec) and marketplace_relationship_unknown(spec):
        blockers.append("unresolved_marketplace_relationship")
    if primary_workflow_requires_integration(spec) and required_integration_unknown(spec):
        blockers.append("unresolved_required_integration")
    blockers.extend(item.id for item in spec.open_decisions if item.impact == "blocking")
    seen: set[str] = set()
    ordered: list[str] = []
    for item in blockers:
        if item not in seen:
            seen.add(item)
            ordered.append(item)
    return ordered


def question_from_decision(decision: OpenDecision) -> DiscoveryQuestion:
    return DiscoveryQuestion(
        decision_id=decision.id,
        text=decision.question,
        options=decision.options,
        recommended_option_id=decision.recommended_option_id,
        allow_free_text=decision.allow_free_text,
        allow_choose_for_me=True,
    )


def question_for_blockers(spec: DiscoverySpec, blockers: list[str]) -> DiscoveryQuestion | None:
    by_id = {item.id: item for item in spec.open_decisions}
    for blocker in blockers:
        if blocker in by_id:
            return question_from_decision(by_id[blocker])
    blocking = [item for item in spec.open_decisions if item.impact == "blocking"]
    if blocking:
        return question_from_decision(blocking[0])
    return None


def grounded_decision_id(blockers: list[str], fallback: str) -> str:
    """Prefer the Core-computed blocker driving `clarify` over the model's own free-text id.

    The same underlying gap (e.g. no primary workflow captured yet) is deterministic given the
    spec, but the model phrases and ids its own question independently each turn. Relabeling to
    the grounded blocker keeps repeated turns over an unchanged gap reporting the same decision,
    without changing what question text the user sees or which decision it answers.
    """
    for blocker in blockers:
        if blocker in DETERMINISTIC_BLOCKER_IDS:
            return blocker
    return fallback


def apply_grounded_decision_id(
    result: DiscoveryTurnResult, blockers: list[str]
) -> DiscoveryTurnResult:
    if result.decision != "clarify" or result.question is None:
        return result
    grounded = grounded_decision_id(blockers, result.question.decision_id)
    if grounded == result.question.decision_id:
        return result
    return result.model_copy(
        update={"question": result.question.model_copy(update={"decision_id": grounded})}
    )


def compile_discovery_brief(
    spec: DiscoverySpec, payload: dict, *, product_name: str, summary: str
) -> str:
    sections = [f"# {product_name}\n", summary]
    product = spec.product
    if spec.original_intent:
        sections.append(f"## Original intent\n{spec.original_intent}")
    if spec.domain or spec.requested_product_shape:
        lines = []
        if spec.domain:
            lines.append(f"Domain: {spec.domain}")
        if spec.requested_product_shape:
            lines.append(f"Requested product shape: {spec.requested_product_shape}")
        sections.append("## Domain signals\n" + "\n".join(lines))
    if product.purpose:
        sections.append(f"## Product purpose\n{product.purpose}")
    if product.first_build_goal:
        sections.append(f"## First build goal\n{product.first_build_goal}")
    if product.target_audience:
        sections.append(
            "## Target audience\n" + "\n".join(f"- {item}" for item in product.target_audience)
        )
    if spec.archetypes:
        sections.append("## Archetypes\n" + ", ".join(spec.archetypes))
    if spec.actors:
        lines = []
        for actor in spec.actors:
            scope = f" Access: {actor.access_scope}." if actor.access_scope else ""
            lines.append(f"- {actor.name}: {actor.description or actor.name}.{scope}")
        sections.append("## People\n" + "\n".join(lines))
    for workflow in spec.workflows:
        steps = " → ".join(step.name for step in workflow.steps) or workflow.description
        body = f"{workflow.description}\n{steps}".strip()
        if workflow.outcome:
            body += f"\nOutcome: {workflow.outcome}"
        sections.append(f"## Workflow: {workflow.name}\n{body}")
    if spec.entities:
        lines = []
        for entity in spec.entities:
            extra = []
            if entity.ownership:
                extra.append(f"ownership: {entity.ownership}")
            if entity.lifecycle:
                extra.append("lifecycle: " + " → ".join(entity.lifecycle))
            suffix = f" ({'; '.join(extra)})" if extra else ""
            lines.append(f"- {entity.name}: {entity.description}{suffix}")
        sections.append("## Core entities\n" + "\n".join(lines))
    if spec.forms:
        sections.append("## Forms\n" + "\n".join(f"- {item}" for item in spec.forms))
    if spec.business_rules:
        sections.append(
            "## Consequential business rules\n"
            + "\n".join(f"- {rule.rule}" for rule in spec.business_rules)
        )
    if spec.authentication:
        auth = spec.authentication
        methods = ", ".join(auth.methods) if auth.methods else "unspecified methods"
        sections.append(
            "## Authentication\n"
            + ("Required. " if auth.required else "Not required. ")
            + methods
            + (f"\n{auth.notes}" if auth.notes else "")
        )
    if spec.tenancy:
        sections.append(
            "## Tenancy\n"
            + spec.tenancy.model
            + (f"\n{spec.tenancy.description}" if spec.tenancy.description else "")
        )
    if spec.billing and (spec.billing.required or spec.billing.notes):
        billing = spec.billing
        sections.append(
            "## Billing\n"
            + "\n".join(
                part
                for part in [
                    f"Required: {billing.required}",
                    f"Model: {billing.model}" if billing.model else "",
                    f"Payer: {billing.payer or billing.billing_owner}"
                    if billing.payer or billing.billing_owner
                    else "",
                    billing.notes or "",
                ]
                if part
            )
        )
    if spec.integrations:
        sections.append(
            "## Integrations\n"
            + "\n".join(
                f"- {item.purpose}"
                + (f" ({item.provider})" if item.provider else "")
                + (" — required for first build" if item.required_for_first_build else " — later")
                for item in spec.integrations
            )
        )
    if spec.notifications:
        sections.append(
            "## Notifications\n"
            + "\n".join(
                f"- {item.trigger} → {item.recipient}"
                + (f" via {item.channel}" if item.channel else "")
                for item in spec.notifications
            )
        )
    if spec.design and (
        spec.design.style or spec.design.brand_reference or spec.design.visual_constraints
    ):
        design = spec.design
        sections.append(
            "## Design\n"
            + "\n".join(
                part
                for part in [
                    design.style or "",
                    f"Brand reference: {design.brand_reference}" if design.brand_reference else "",
                    *design.visual_constraints,
                ]
                if part
            )
        )
    if spec.explicit_exclusions:
        sections.append(
            "## Explicit exclusions\n"
            + "\n".join(f"- {item}" for item in spec.explicit_exclusions)
        )
    if spec.requires_configuration:
        sections.append(
            "## Requires configuration\n"
            + "\n".join(f"- {item}" for item in spec.requires_configuration)
        )
    if spec.requirements:
        sections.append(
            "## Explicit constraints\n" + "\n".join(f"- {item}" for item in spec.requirements)
        )
    if spec.acceptance_outcomes:
        sections.append(
            "## Acceptance outcomes\n" + "\n".join(f"- {item}" for item in spec.acceptance_outcomes)
        )
    if spec.assumptions:
        sections.append(
            "## Explicit assumptions\n"
            + "\n".join(
                f"- {item.statement} (basis: {item.basis}"
                f", delegated={item.basis == 'user_delegated_default'})"
                for item in spec.assumptions
            )
        )
    if spec.deferred_scope:
        sections.append(
            "## Deferred scope\n" + "\n".join(f"- {item}" for item in spec.deferred_scope)
        )
    sections.append(
        "## Source requirements and discovery answers\n"
        "These are product requirements, not platform policy. "
        "Later clarifications override earlier requests.\n"
        + "\n\n".join(
            message["content"]
            for message in payload.get("messages", [])
            if message["role"] == "user"
        )
    )
    sections.append(
        "## Implementation autonomy\n"
        "Discovery defined product intent, actors, workflows, core entities, consequential "
        "rules, permissions, tenancy, billing, and integrations. The validated App Block / "
        "Feature Pack composition is architecture authority. Codegen implements that "
        "composition using customer terminology in the UI and canonical platform identities "
        "at runtime. Codegen chooses screens, conventional fields, CRUD UX, navigation, and "
        "other reversible implementation details. Codegen must not invent backends, tables, "
        "APIs, or capability substitutes from the original customer prompt."
    )
    return "\n\n".join(section for section in sections if section.strip())


def public_summary(spec: DiscoverySpec, product_name: str) -> str:
    goal = spec.product.first_build_goal or spec.product.purpose
    actors = ", ".join(actor.name for actor in spec.actors[:4])
    entities = ", ".join(entity.name for entity in spec.entities[:6])
    parts = [f"{product_name}: {goal}".strip(": ")]
    if actors:
        parts.append(f"People: {actors}.")
    if entities:
        parts.append(f"Built around {entities}.")
    return " ".join(parts)[:3000]


def persistable_result(
    turn: DiscoveryTurnResult, payload: dict, *, specification: str
) -> DiscoveryResult:
    spec = turn.spec
    product_name = working_product_name(spec)
    if spec.product.name_status == "unknown" or not (spec.product.name or "").strip():
        spec = spec.model_copy(
            update={
                "product": spec.product.model_copy(
                    update={"name": product_name, "name_status": "working"}
                )
            }
        )
        turn = turn.model_copy(update={"spec": spec})
    summary = public_summary(spec, product_name)
    question = turn.question
    payload = {
        key: value
        for key, value in turn.model_dump().items()
        if key in DiscoveryTurnResult.model_fields
    }
    return DiscoveryResult(
        **payload,
        product_name=product_name,
        summary=summary,
        specification=specification,
        assumptions=[item.statement for item in spec.assumptions],
        questions=[question.text] if question else [],
    )


def finalize_discovery_result(result: DiscoveryTurnResult, payload: dict) -> DiscoveryResult:
    if result.decision in {"blocked", "out_of_scope"}:
        result = result.model_copy(
            update={"template_ref": None, "requested_capabilities": [], "question": None}
        )
        return persistable_result(result, payload, specification="")
    if result.decision in {"ready", "clarify"}:
        result = result.model_copy(update={"spec": apply_fidelity_hygiene(result.spec, payload)})
        # The user describes outcomes in customer language. Convert those words into the
        # internal capability contract before the plan is persisted, so Codegen and its strict
        # conformance checks agree even when the model omits derived capability fields.
        result = result.model_copy(
            update={"spec": normalize_capability_intent(result.spec, payload)}
        )
        result = result.model_copy(
            update={
                "requested_capabilities": reconcile_requested_capabilities(
                    result.spec, result.requested_capabilities
                )
            }
        )
    if result.template_fit != "strong":
        result = result.model_copy(update={"template_ref": None})
    if result.template_ref:
        candidate = next(
            (item for item in payload.get("templates", []) if item["ref"] == result.template_ref),
            None,
        )
        # The template must belong to the chosen project type. The worker rejects a mismatch
        # as invalid_template_selection, so drop it here and fall back to the base boilerplate.
        if (
            not candidate
            or not candidate.get("auto_selectable", False)
            or candidate.get("framework") != result.project_type
        ):
            result = result.model_copy(
                update={
                    "template_ref": None,
                    "template_fit": "none",
                    "template_reason": (
                        "Catalog metadata does not establish workflow fit; "
                        "use the base boilerplate."
                    ),
                }
            )
    blockers = validate_discovery_readiness(result.spec)
    if result.decision == "clarify" and should_promote_ready(result, payload, blockers):
        result = result.model_copy(
            update={
                "decision": "ready",
                "question": None,
                "message": public_summary(result.spec, working_product_name(result.spec)),
                "readiness": DiscoveryReadiness(
                    buildable=True,
                    blocking_decision_ids=[],
                    reason="The remaining question concerns conventional implementation details.",
                ),
            }
        )
    if result.decision == "ready" and blockers:
        question = result.question or question_for_blockers(result.spec, blockers)
        result = result.model_copy(
            update={
                "decision": "clarify",
                "question": question,
                "readiness": DiscoveryReadiness(
                    buildable=False,
                    blocking_decision_ids=blockers,
                    reason="Consequential product decisions remain unresolved.",
                ),
                "template_ref": None,
            }
        )
        result = apply_grounded_decision_id(result, blockers)
        return persistable_result(result, payload, specification="")
    if result.decision != "ready":
        result = result.model_copy(update={"template_ref": None})
        if result.decision == "clarify" and result.question is None:
            question = question_for_blockers(
                result.spec, [item.id for item in result.spec.open_decisions]
            )
            if question:
                result = result.model_copy(update={"question": question})
        result = apply_grounded_decision_id(result, blockers)
        return persistable_result(result, payload, specification="")
    result = result.model_copy(
        update={
            "question": None,
            "readiness": DiscoveryReadiness(
                buildable=True,
                blocking_decision_ids=[],
                reason=result.readiness.reason or "First version is buildable.",
            ),
        }
    )
    product_name = working_product_name(result.spec)
    summary = public_summary(result.spec, product_name)
    specification = compile_discovery_brief(
        result.spec, payload, product_name=product_name, summary=summary
    )
    return persistable_result(result, payload, specification=specification)


def discovery_assistant_message(result: DiscoveryTurnResult) -> str:
    return result.message
