"""Cheap intake before allocating a project/build. Core owns the admission decision."""

import asyncio
import hashlib
import json
import time

import structlog

from src.config import get_settings
from src.core.exceptions import P2HSError, ValidationError
from src.domain.projects.intake_schemas import (
    INTAKE_MESSAGES,
    IntakeCandidate,
    ProjectUnderstanding,
)

logger = structlog.get_logger()
POLICY_VERSION = "1"


class IntakeUnavailableError(P2HSError):
    def __init__(self):
        super().__init__(
            "We couldn't check your idea just now. Please try again in a moment; "
            "no build has started.",
            "INTAKE_UNAVAILABLE",
        )


def require_allowed(result: ProjectUnderstanding | None) -> None:
    if result is not None and result.decision != "allow":
        raise ValidationError(
            INTAKE_MESSAGES[result.decision],
            error=f"project_{result.decision}",
        )


async def assess_project_intent(
    prompt: str,
    *,
    candidates: list[IntakeCandidate] | None = None,
    existing_app: bool = False,
    has_attachments: bool = False,
) -> ProjectUnderstanding | None:
    settings = get_settings()
    if not settings.project_intake_enabled:
        return None
    if not prompt.strip() and not has_attachments:
        return ProjectUnderstanding(
            decision="clarify",
            category="unspecified",
            summary="",
            requested_features=[],
            template_ref=None,
            confidence=1,
        )
    if len(prompt) > settings.project_intake_max_chars:
        raise ValidationError(
            "Start with a shorter brief covering the app's purpose and main features. "
            "You can add details as we build.",
            error="project_brief_too_long",
        )
    if not settings.openai_api_key:
        raise IntakeUnavailableError()

    from redis.asyncio import Redis

    from src.integrations.project_intake_client import understand_project

    pool = candidates or []
    cache_input = json.dumps(
        [
            POLICY_VERSION,
            settings.project_intake_model,
            prompt,
            existing_app,
            has_attachments,
            [c.model_dump() for c in pool],
        ],
        sort_keys=True,
    )
    cache_key = "project-intake:" + hashlib.sha256(cache_input.encode()).hexdigest()
    try:
        # A shared budget prevents an anonymous endpoint from generating unlimited spend.
        # Cache keys contain hashes, never prompt text or user identifiers.
        async with Redis.from_url(
            settings.redis_url, socket_timeout=2, socket_connect_timeout=2
        ) as redis:
            cached = await redis.get(cache_key)
            if cached:
                return ProjectUnderstanding.model_validate_json(cached)
            minute = int(time.time()) // 60
            used = await redis.eval(
                "local n=redis.call('INCR',KEYS[1]); "
                "if n==1 then redis.call('EXPIRE',KEYS[1],120) end; return n",
                1,
                f"project-intake:budget:{minute}",
            )
            if used > settings.project_intake_calls_per_minute:
                raise IntakeUnavailableError()
            result = await asyncio.wait_for(
                understand_project(
                    prompt,
                    pool,
                    existing_app=existing_app,
                    has_attachments=has_attachments,
                    settings=settings,
                ),
                timeout=settings.project_intake_timeout_seconds,
            )
            if result.confidence < 0.65:
                result.decision = "clarify"
            if result.decision != "allow":
                result.template_ref = None
                result.requested_features = []
            elif result.template_ref not in {c.ref for c in pool}:
                result.template_ref = None
            await redis.set(cache_key, result.model_dump_json(), ex=600)
            logger.info(
                "project_intake_result",
                decision=result.decision,
                category=result.category,
                policy_version=POLICY_VERSION,
            )
            return result
    except Exception as exc:
        # Never log provider exception bodies (they can include the submitted prompt).
        logger.warning("project_intake_unavailable", exception_type=type(exc).__name__)
        raise IntakeUnavailableError() from None
