"""AI-assisted template matching for project-creation intent.

Used by ``ProjectService._find_matching_templates`` to pick a starter template from a
short candidate list based on the user's free-text project description, going beyond
what bag-of-words keyword overlap can catch (e.g. "handmade jewelry shop" matching a
template tagged "ecommerce, boutique" with zero literal term overlap).
"""
from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Optional

import structlog

from src.config import get_settings

logger = structlog.get_logger()

# Below this confidence, callers should fall back to keyword matching / the framework default
# rather than force a template switch on a shaky guess.
TEMPLATE_MATCH_CONFIDENCE_THRESHOLD = 0.6

_SYSTEM_PROMPT = (
    "You match a user's project description to the single best-fitting starter template "
    "from a short candidate list, or decide none fit well. Only pick a template when it is "
    "genuinely a good starting point for what the user described — do not force a match "
    "just because it's the closest of a bad set. "
    'Respond with strict JSON: {"template_ref": string or null, "confidence": number between '
    '0 and 1, "reasoning": short one-sentence string}.'
)


@dataclass
class TemplateMatchCandidate:
    ref: str
    name: str
    description: Optional[str]
    tags: list[str]


@dataclass
class TemplateMatchResult:
    template_ref: str
    confidence: float
    reasoning: str


async def match_template_with_ai(
    intent: str,
    candidates: list[TemplateMatchCandidate],
) -> Optional[TemplateMatchResult]:
    """Ask an LLM to pick the best-fitting template for ``intent``, or return None.

    Best-effort: a missing API key, provider error, or malformed response is treated as
    "no match" so template selection always degrades gracefully to keyword matching / the
    framework default rather than blocking project creation.
    """
    if not intent.strip() or not candidates:
        return None

    settings = get_settings()
    if settings.project_intake_enabled:
        # Discovery chooses the final starter. Initial provisioning uses the framework default.
        return None
    if not getattr(settings, "openai_api_key", None):
        return None

    try:
        from openai import AsyncOpenAI

        client = AsyncOpenAI(api_key=settings.openai_api_key)
        candidates_text = "\n".join(
            f"- ref={c.ref} | name={c.name} | tags={', '.join(c.tags)} | description={c.description or ''}"
            for c in candidates
        )
        response = await client.chat.completions.create(
            model=settings.openai_model,
            messages=[
                {"role": "system", "content": _SYSTEM_PROMPT},
                {
                    "role": "user",
                    "content": f"User's project description:\n{intent}\n\nCandidate templates:\n{candidates_text}",
                },
            ],
            response_format={"type": "json_object"},
            temperature=0.1,
        )
        content = response.choices[0].message.content
        parsed = json.loads(content)
    except Exception as exc:
        logger.warning("template_ai_match_failed", error=str(exc))
        return None

    template_ref = parsed.get("template_ref")
    confidence = parsed.get("confidence")
    if not isinstance(template_ref, str) or not template_ref.strip():
        return None
    if not isinstance(confidence, (int, float)):
        return None
    valid_refs = {c.ref for c in candidates}
    if template_ref not in valid_refs:
        logger.warning("template_ai_match_invalid_ref", template_ref=template_ref)
        return None

    return TemplateMatchResult(
        template_ref=template_ref,
        confidence=float(confidence),
        reasoning=str(parsed.get("reasoning") or "").strip()[:500],
    )
