"""OpenAI GPT-4o client — the 20-idea generator (stage 8).

Given the verified facts, the voice profile, and the recent news, generate
exactly 20 city-specific crowd-work ideas. Returns 20 `GeneratedIdea` rows.

Uses the OpenAI-compatible /v1/chat/completions endpoint with strict JSON
output. We avoid pulling in the full openai SDK — the only call shape we need
is one completions request, so a plain httpx call keeps the dependency surface
small (matches leads-importer + community-system conventions).
"""

from __future__ import annotations

import json
import logging
from datetime import date
from typing import Any

import httpx

from app.config import settings
from app.models import FactCheckResult, GeneratedIdea, RemovedFact, VerifiedFact, VoiceProfile
from app.providers._common import (
    JsonExtractionError,
    _build_semaphore,
    complete_chat,
    extract_json,
)

logger = logging.getLogger(__name__)

_OPENAI_URL = "https://api.openai.com/v1/chat/completions"


class OpenAIClient:
    def __init__(self) -> None:
        self._client = httpx.AsyncClient(timeout=settings.OPENAI_TIMEOUT)
        self._sem = _build_semaphore(settings.OPENAI_MAX_CONCURRENCY)

    async def close(self) -> None:
        await self._client.aclose()

    def _headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {settings.OPENAI_API_KEY}",
            "Content-Type": "application/json",
        }

    # ── Stage 7: fact_check (replaces Gemini per Phase 12 decision) ─────

    async def fact_check(
        self,
        *,
        city: str,
        country: str,
        show_date: date,
        city_intel: dict[str, Any],
        recent_news: list[dict[str, Any]],
    ) -> FactCheckResult:
        """Verify claims produced by Perplexity before they reach idea-gen.

        Uses GPT-4o (per Phase 12 decision: GPT-4o replaces Gemini for the
        fact-check so the pipeline runs with one fewer vendor key). Same
        contract as the original Gemini stage: returns verified_facts + removed_facts.
        """
        system = (
            "You are a fact-checker for a stand-up comedy brief. You will be "
            "given city-intelligence and recent-news claims produced by another "
            "model. Your job is to KEEP only claims that are verifiable today and "
            "low-risk to mention on stage. REMOVE claims that are unverifiable, "
            "disputed, dated, or risky. Return STRICT JSON only."
        )
        user = (
            f"Show: {city}, {country} on {show_date.isoformat()}.\n"
            f"City intelligence JSON:\n{json.dumps(city_intel, ensure_ascii=False, default=str)}\n"
            f"Recent news JSON:\n{json.dumps(recent_news, ensure_ascii=False, default=str)}\n"
            "Return a JSON object with two keys:\n"
            '  "verified_facts": array of {claim, confidence ("high"|"medium"|"low"), source}\n'
            '  "removed_facts": array of {claim, removed_reason}\n'
            "Do not invent sources. If you have no source, set source to null."
        )
        payload = {
            "model": settings.OPENAI_FACT_CHECK_MODEL,
            "messages": [
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
            "temperature": 0.0,
            "response_format": {"type": "json_object"},
        }
        async with self._sem:
            text = await complete_chat(
                client=self._client,
                url=_OPENAI_URL,
                headers=self._headers(),
                payload=payload,
                timeout=float(settings.OPENAI_TIMEOUT),
                provider_name="openai",
            )
        try:
            parsed = extract_json(text)
        except JsonExtractionError:
            logger.warning("openai fact_check returned non-JSON; assuming zero trust")
            return FactCheckResult(
                verified_facts=[],
                removed_facts=[RemovedFact(
                    claim="<openai returned non-JSON>",
                    removed_reason="unparseable response",
                )],
            )
        verified = []
        for item in parsed.get("verified_facts", []):
            conf = str(item.get("confidence") or "medium").lower()
            if conf not in ("high", "medium", "low"):
                conf = "medium"
            verified.append(VerifiedFact(
                claim=str(item.get("claim") or "").strip()[:1000],
                confidence=conf,
                source=(str(item.get("source")) if item.get("source") else None),
            ))
        removed = []
        for item in parsed.get("removed_facts", []):
            removed.append(RemovedFact(
                claim=str(item.get("claim") or "").strip()[:1000],
                removed_reason=str(item.get("removed_reason") or "").strip()[:500],
            ))
        return FactCheckResult(verified_facts=verified, removed_facts=removed)

    # ── Stage 8: generate_twenty_ideas ─────────────────────────────────

    async def generate_twenty_ideas(
        self,
        *,
        city: str,
        country: str,
        show_date: date,
        voice: VoiceProfile,
        verified_facts: list[dict[str, Any]],
        city_intel: dict[str, Any],
        competitor_warnings: list[dict[str, Any]],
        previous_show_summary: str | None,
        recent_transcripts: list[dict[str, Any]] | None = None,
    ) -> list[GeneratedIdea]:
        system = (
            "You are a stand-up comedy opening-ideas writer for a touring "
            f"comedian ({voice.display_name}). You produce crowd-work and "
            "opener material that is specific to the host city. Stay inside "
            "the comedian's voice and persona. Do not invent unverified facts."
        )
        voice_block = json.dumps({
            "persona": voice.persona,
            "tone": voice.tone,
            "signature_bits": voice.signature_bits,
            "topics_to_lean_into": voice.topics_to_lean_into,
            "sensitivities_to_avoid": voice.sensitivities_to_avoid,
            "vocabulary": voice.vocabulary,
        }, ensure_ascii=False)
        # Build a "recent material" block from YouTube transcripts so the 20
        # ideas do NOT overlap with the comedian's most recent posted bits.
        recent_block = "no recent material provided"
        if recent_transcripts:
            compact = [
                {"title": v.get("title", ""), "transcript_excerpt": (v.get("transcript") or "")[:1500]}
                for v in recent_transcripts
            ]
            recent_block = json.dumps(compact, ensure_ascii=False, default=str)
        user = (
            f"Show: {city}, {country} on {show_date.isoformat()}.\n"
            f"Voice profile JSON:\n{voice_block}\n"
            f"Verified facts JSON (the only facts you may use):\n{json.dumps(verified_facts, ensure_ascii=False, default=str)}\n"
            f"City intelligence JSON:\n{json.dumps(city_intel, ensure_ascii=False, default=str)}\n"
            f"Competitor warnings JSON:\n{json.dumps(competitor_warnings, ensure_ascii=False, default=str)}\n"
            f"Previous show in this city: {previous_show_summary or 'first show in this city'}\n"
            f"Max Amini's recent posted material (DO NOT repeat these angles/bits):\n{recent_block}\n"
            "Generate EXACTLY 20 city-specific crowd-work opener ideas. "
            "Each idea MUST use one or more verified facts and the city name. "
            "Avoid topics that overlap with the recent material above. "
            "Return STRICT JSON: an array of objects with keys "
            '"title", "angle", "city_specificity", "risk_note" (nullable).'
        )
        payload = {
            "model": settings.OPENAI_GENERATION_MODEL,
            "messages": [
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
            "temperature": 0.6,
            "response_format": {"type": "json_object"},
        }
        async with self._sem:
            text = await complete_chat(
                client=self._client,
                url=_OPENAI_URL,
                headers=self._headers(),
                payload=payload,
                timeout=float(settings.OPENAI_TIMEOUT),
                provider_name="openai",
            )
        try:
            parsed = extract_json(text)
        except JsonExtractionError as exc:
            logger.warning("openai returned non-JSON: %s", exc)
            return []
        # OpenAI's strict mode wraps the array; unwrap "ideas" if present.
        if isinstance(parsed, dict):
            array_source = next(
                (parsed[k] for k in ("ideas", "crowd_work", "topics") if isinstance(parsed.get(k), list)),
                [],
            )
        elif isinstance(parsed, list):
            array_source = parsed
        else:
            array_source = []
        results: list[GeneratedIdea] = []
        for item in array_source[:20]:
            if not isinstance(item, dict):
                continue
            results.append(GeneratedIdea(
                title=str(item.get("title") or "").strip()[:300],
                angle=str(item.get("angle") or "").strip()[:1000],
                city_specificity=str(item.get("city_specificity") or "").strip()[:500],
                risk_note=(str(item.get("risk_note")).strip()[:500]
                          if item.get("risk_note") else None),
            ))
        return results