"""Core-owned, resumable discovery. Only approved output may change the starter."""

from uuid import UUID

from src.config import get_settings
from src.core.exceptions import (
    ConflictError,
    ForbiddenError,
    NotFoundError,
    ValidationError,
)
from src.domain.projects.discovery_repository import DiscoveryRepository
from src.domain.projects.discovery_schemas import (
    DiscoveryAnswerInput,
    DiscoveryInput,
    public_discovery,
)

DISCOVERY_TURN = "discovery_turn"
DISCOVERY_APPLY = "discovery_apply"


def discovery_enabled_for_workspace(settings, workspace_id: UUID) -> bool:
    if not settings.project_intake_enabled:
        return False
    allowed = {
        value.strip().lower() for value in settings.project_discovery_workspace_ids.split(",")
    }
    return settings.project_discovery_public_enabled or str(workspace_id).lower() in allowed


def resolve_turn_input(state: dict, body: DiscoveryInput, message: str) -> tuple[str, dict]:
    answer = body.answer
    if body.action == "answer" and answer is None:
        answer = DiscoveryAnswerInput(type="message", content=message)
    question = (state.get("result") or {}).get("question") or {}
    decision_id = (
        (answer.decision_id if answer else None)
        or (answer.question_id if answer else None)
        or question.get("decision_id")
        or question.get("id")
    )
    if answer and answer.type == "choose_for_me":
        return (
            "Choose the recommended setup for me.",
            {"type": "choose_for_me", "decision_id": decision_id},
        )
    if answer and answer.type == "option":
        options = {item.get("id"): item for item in question.get("options") or []}
        option = options.get(answer.option_id or "") or {}
        label = option.get("label") or answer.option_id or ""
        if not label:
            raise ValidationError("Choose one of the suggested options")
        return label, {
            "type": "option",
            "decision_id": decision_id,
            "option_id": answer.option_id,
            "label": label,
        }
    content = ((answer.content if answer else None) or message).strip()
    if not content:
        content = state.get("initial_prompt", "").strip() if not state.get("messages") else ""
    return content, {"type": "message", "content": content}


def initial_discovery(prompt: str, locked_type: str | None = None) -> dict:
    return {
        "version": 0,
        "status": "pending",
        "messages": [],
        "initial_prompt": prompt,
        "locked_type": locked_type,
        "attachment_ids": [],
    }


def require_discovery_complete(project):
    state = getattr(project, "discovery_json", None)
    if state and state.get("status") != "completed":
        raise ValidationError(
            "Finish project discovery before starting a build.", error="discovery_required"
        )


async def require_discovery_access(db, project, user_id):
    from src.domain.billing.access_policy import BillingAccessPolicy
    from src.domain.billing.repository import SubscriptionRepository
    from src.domain.workspaces.repository import MembershipRepository, WorkspaceRepository

    if not project:
        raise NotFoundError("Project not found")
    if not await MembershipRepository(db).has_permission(
        project.workspace_id, user_id, "projects.write"
    ):
        raise ForbiddenError()
    # Reuse Core policy, including no-charge staff/free plans and complimentary access.
    await BillingAccessPolicy(
        SubscriptionRepository(db), WorkspaceRepository(db)
    ).require_active_billing(project.workspace_id)


async def available_templates(db, workspace_id):
    from src.domain.admin.repository import SubscriptionPlanRepository
    from src.domain.billing.plan_quota_service import PlanQuotaService
    from src.domain.billing.repository import SubscriptionRepository
    from src.domain.templates.repository import TemplateRepository
    from src.domain.workspaces.repository import WorkspaceRepository

    quota = PlanQuotaService(
        WorkspaceRepository(db), SubscriptionRepository(db), SubscriptionPlanRepository(db)
    )
    return await TemplateRepository(db).list_for_plan(await quota.plan_slug(workspace_id))


class DiscoveryService:
    def __init__(self, db):
        self.db = db
        self.repo = DiscoveryRepository(db)

    async def submit(self, project_id: UUID, user_id: UUID, body: DiscoveryInput):
        from src.domain.projects.repository import ProjectJobOutboxRepository
        from src.domain.prompt_media.repository import PromptMediaRepository

        project = await self.repo.get(project_id, lock=True)
        await require_discovery_access(self.db, project, user_id)
        state = dict(project.discovery_json or {})
        if not state:
            raise ValidationError("This project does not require discovery")
        if str(body.request_id) == state.get("last_request_id"):
            return public_discovery(state)
        if state["version"] != body.expected_version:
            raise ConflictError("Discovery changed. Refresh and try again.")
        if body.action == "reset":
            # Preserve the user's conversation and project identity, but discard the accepted
            # model result and any apply handoff. Bumping the version fences off a late worker
            # from the abandoned processing/applying state; the next message starts a fresh model
            # turn and finalize_discovery_result recompiles the plan with current Core rules.
            state.update(
                status="pending",
                result=None,
                error=None,
                error_code=None,
                approved_by=None,
                application_id=None,
                initial_run_id=None,
                reset_for_retry=True,
                actor_id=str(user_id),
            )
            state.update(version=state["version"] + 1, last_request_id=str(body.request_id))
            project.discovery_json = state
            await self.db.commit()
            return public_discovery(state)
        if state["status"] in {"processing", "applying", "completed"}:
            raise ConflictError("Discovery is already processing or complete")
        if body.action == "approve":
            if state["status"] != "ready":
                raise ValidationError("Answer the discovery questions before building")
            state.update(
                status="applying",
                approved_by=str(user_id),
                application_id=str(body.request_id),
                error=None,
            )
            job_kind = DISCOVERY_APPLY
        else:
            message = body.message.strip()
            if body.action == "retry":
                if state["status"] != "failed":
                    raise ValidationError("Only failed discovery work can be retried")
                job_kind = DISCOVERY_APPLY if state.get("approved_by") else DISCOVERY_TURN
            else:
                message, latest_input = resolve_turn_input(state, body, message)
                if not message:
                    raise ValidationError("Describe the app you want to build")
                messages = [*state["messages"], {"role": "user", "content": message}]
                if (
                    sum(len(m["content"]) for m in messages)
                    > get_settings().project_intake_max_chars
                ):
                    raise ValidationError("Discovery is too long. Shorten your answer.")
                if body.attachment_ids:
                    assets = await PromptMediaRepository(self.db).list_by_ids_for_project(
                        body.attachment_ids, project_id
                    )
                    if len(assets) != len(set(body.attachment_ids)) or any(
                        a.prompt_id for a in assets
                    ):
                        raise ValidationError("Unknown or already used attachment")
                    state["attachment_ids"] = list(
                        dict.fromkeys(
                            [*state.get("attachment_ids", []), *map(str, body.attachment_ids)]
                        )
                    )
                state.update(messages=messages, actor_id=str(user_id), latest_input=latest_input)
                job_kind = DISCOVERY_TURN
            state.update(
                status="applying" if job_kind == DISCOVERY_APPLY else "processing", error=None
            )
        state.update(version=state["version"] + 1, last_request_id=str(body.request_id))
        project.discovery_json = state
        if job_kind != DISCOVERY_APPLY or project.status == "ready":
            await ProjectJobOutboxRepository(self.db).insert_pending(
                project_id, job_kind, {"version": state["version"]}
            )
        await self.db.commit()
        from src.workers.enqueue import enqueue_project_outbox_dispatch_nudge

        try:
            await enqueue_project_outbox_dispatch_nudge(defer_seconds=0)
        except Exception:
            pass  # Durable outbox cron is the fallback.
        return public_discovery(state)


async def enqueue_approved_discovery_after_setup(db, project_id):
    """Called in the setup-ready transaction; shares the approval row lock."""
    from src.domain.projects.repository import ProjectJobOutboxRepository

    project = await DiscoveryRepository(db).get(project_id, lock=True)
    state = (project.discovery_json or {}) if project else {}
    if state.get("status") == "applying" and state.get("approved_by"):
        state = {**state, "version": state["version"] + 1}
        project.discovery_json = state
        await ProjectJobOutboxRepository(db).insert_pending(
            project_id, DISCOVERY_APPLY, {"version": state["version"]}
        )
