"""Sync and async BannerResource classes."""

from __future__ import annotations

import logging
import time
from pathlib import Path
from typing import TYPE_CHECKING

from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential

from ..._api.client import BaseResource, AsyncBaseResource
from ...utils.json_extractor import JSONExtractor
from ._types import BannerPrompt, BannerResult
from ._prompts import build_system_prompt
from ._utils import build_user_context, save_image
from .presets import (
    BannerSize,
    BannerStyle,
    BannerModel,
    BannerLLMModel,
    BannerQuality,
    BannerImageStyle,
    ImageModelFamily,
    PRESETS,
    resolve_model_family,
    DEFAULT_SIZE,
    DEFAULT_STYLE,
    DEFAULT_MODEL,
    DEFAULT_LLM_MODEL,
    DEFAULT_QUALITY,
    DEFAULT_IMAGE_STYLE,
)

if TYPE_CHECKING:
    from ..._config import SDKConfig

logger = logging.getLogger(__name__)

_EnumT = type  # just for readability


def _is_transient_llm(exc: BaseException) -> bool:
    """Return True for transient LLM errors worth retrying (timeout, 429, 5xx)."""
    import httpx

    if isinstance(exc, (TimeoutError, ConnectionError, OSError)):
        return True
    if isinstance(exc, httpx.TimeoutException):
        return True
    if isinstance(exc, httpx.NetworkError):
        return True
    # openai / httpx HTTPStatusError — retry 429, 500-503
    status = None
    if hasattr(exc, "status_code"):
        status = exc.status_code
    elif hasattr(exc, "response") and hasattr(exc.response, "status_code"):
        status = exc.response.status_code
    if status in {429, 500, 502, 503, 504}:
        return True
    return False


def _coerce(enum_cls, value, default):
    """Coerce a string or enum value into an enum member, fallback to default."""
    if isinstance(value, enum_cls):
        return value
    try:
        return enum_cls(value)
    except ValueError:
        logger.warning("Unknown %s value %r, using default %r", enum_cls.__name__, value, default)
        return default


# ── Sync resource ─────────────────────────────────────────────────────────────

class BannerResource(BaseResource):
    """Banner / hero image generator.

    Combines model-aware LLM prompt engineering with image generation.
    The LLM always runs — it knows the target image model's prompt rules
    and adapts the output accordingly (DALL-E vs Flux vs Kontext conventions).

    Saves the result directly to disk when output= is specified.
    """

    def __init__(self, config: "SDKConfig") -> None:
        super().__init__(config)

    def generate(
        self,
        *,
        # Content source — at least one required (unless prompt= is provided)
        source: str | Path | None = None,
        title: str | None = None,
        description: str | None = None,
        github_url: str | None = None,

        # Provide your own final image prompt — LLM is skipped, goes straight to image gen
        prompt: str | None = None,

        # Visual style enum — steers LLM aesthetic direction, does NOT skip LLM
        preset: BannerStyle | str = DEFAULT_STYLE,

        # Output
        output: str | Path | None = None,

        # Image generation options
        size: BannerSize | str = DEFAULT_SIZE,
        model: BannerModel | str = DEFAULT_MODEL,
        quality: BannerQuality | str = DEFAULT_QUALITY,
        style: BannerImageStyle | str = DEFAULT_IMAGE_STYLE,

        # LLM options
        llm_model: BannerLLMModel | str = DEFAULT_LLM_MODEL,

        # Dev helpers
        dry_run: bool = False,
    ) -> BannerResult:
        """Generate a banner image for a project.

        The LLM always runs (unless prompt= is provided directly) and writes
        an optimised image generation prompt based on:
        - The project context (README, title, description)
        - The visual style direction (preset= enum)
        - The target image model's prompt rules (DALL-E vs Flux vs Kontext)

        Args:
            source: Path to a local README.md or other markdown file.
                    Can also be a GitHub file URL.
            title: Project title (context for LLM).
            description: Short project description (context for LLM).
            github_url: GitHub repository URL — README.md is fetched automatically.
            prompt: Final image generation prompt. Skips LLM entirely — use when
                    you want full control over the prompt. Otherwise let the LLM
                    write it from your project context.
            preset: Visual style direction for the LLM. Use BannerStyle enum:
                    BannerStyle.GLASSMORPHISM, BannerStyle.ISOMETRIC, BannerStyle.AI_NETWORK,
                    BannerStyle.CYBERPUNK, BannerStyle.BLUEPRINT, BannerStyle.MINIMAL,
                    BannerStyle.SPEED, BannerStyle.PIXAR, BannerStyle.HOLOGRAPHIC,
                    BannerStyle.GRADIENT_MESH.
                    String values (e.g. "glassmorphism") also accepted.
                    LLM always runs — preset steers the aesthetic, not replaces the LLM.
            output: Local file path to save the image (e.g. "assets/banner.png").
                    If not set, the image is generated but not saved locally.
            size: Image size preset (BannerSize enum or string):
                  "github" (1792x1024), "social" (1200x630), "og" (1200x630),
                  "wide" (1792x512), "square" (1024x1024).
            model: Image generation model. Aliases: @cheap, @balanced, @smart, @fast.
                   The model family is auto-detected to set correct prompt rules.
            quality: Image quality: "standard" or "hd".
            style: Image style: "natural" or "vivid".
            llm_model: LLM model for prompt generation. Aliases: @smart, @balanced.
            dry_run: If True, return the LLM-generated prompt without calling image gen API.

        Returns:
            BannerResult with image_url, prompt, saved_to path, cost, and duration.

        Raises:
            ValueError: If no project context is provided (and prompt= is not set).
        """
        t0 = time.monotonic()

        # Resolve all enum params → typed enums, then extract .value strings for APIs
        size_e: BannerSize       = _coerce(BannerSize, size, DEFAULT_SIZE)
        preset_e: BannerStyle    = _coerce(BannerStyle, preset, DEFAULT_STYLE)
        model_e: BannerModel     = _coerce(BannerModel, model, DEFAULT_MODEL)
        quality_e: BannerQuality = _coerce(BannerQuality, quality, DEFAULT_QUALITY)
        style_e: BannerImageStyle = _coerce(BannerImageStyle, style, DEFAULT_IMAGE_STYLE)
        llm_e: BannerLLMModel    = _coerce(BannerLLMModel, llm_model, DEFAULT_LLM_MODEL)

        dimensions = size_e.dimensions()
        model_str = model_e.value
        quality_str = quality_e.value
        style_str = style_e.value
        llm_model_str = llm_e.value

        total_cost = 0.0
        used_llm_model: str | None = None

        # ── Step 1: Build prompt ──────────────────────────────────────────────
        if prompt:
            # User provided their own final prompt — skip LLM
            final_prompt = prompt
            logger.info("Using provided prompt directly (LLM skipped)")
        else:
            # LLM always runs here
            if not any([source, title, description, github_url]):
                raise ValueError(
                    "Provide at least one of: source, title, description, github_url. "
                    "Or pass prompt= to skip LLM entirely."
                )
            user_ctx = build_user_context(
                source=source,
                title=title,
                description=description,
                github_url=github_url,
            )
            # Resolve model family → determines which prompt rules the LLM follows
            family = resolve_model_family(model_str)
            final_prompt, llm_cost = self._enhance_prompt(
                user_ctx, llm_model_str, family=family, style=preset_e
            )
            total_cost += llm_cost
            used_llm_model = llm_model_str

        logger.info("Banner prompt (%d chars): %s…", len(final_prompt), final_prompt[:80])

        if dry_run:
            return BannerResult(
                image_url="",
                prompt=final_prompt,
                preset=preset_e.value,
                size=dimensions,
                model=model_str,
                llm_model=used_llm_model,
                cost_usd=total_cost,
                duration_ms=int((time.monotonic() - t0) * 1000),
                saved_to=None,
            )

        # ── Step 2: Generate image ────────────────────────────────────────────
        image_result = self._get_sdk().image_gen.generate(
            final_prompt,
            model=model_str,
            size=dimensions,
            quality=quality_str,
            style=style_str,
        )
        image_url = str(image_result.image_cdn_url or image_result.image_url or "")
        total_cost += float(image_result.cost_usd or 0)
        logger.info("Banner generated: url=%s cost=$%.4f", image_url, total_cost)

        # ── Step 3: Save to disk (optional) ──────────────────────────────────
        saved_to: Path | None = None
        if output and image_url:
            saved_to = save_image(image_url, output)

        return BannerResult(
            image_url=image_url,
            prompt=final_prompt,
            preset=preset_e.value,
            size=dimensions,
            model=model_str,
            llm_model=used_llm_model,
            cost_usd=total_cost,
            duration_ms=int((time.monotonic() - t0) * 1000),
            saved_to=saved_to,
        )

    def _get_sdk(self):
        from ..._client import SDKRouter
        return SDKRouter(
            api_key=self._config.api_key,
            llm_url=self._config.llm_url,
            api_url=self._config.api_url,
            audio_url=self._config.audio_url,
            use_self_hosted=self._config.use_self_hosted,
            timeout=self._config.timeout,
            max_retries=self._config.max_retries,
        )

    def _enhance_prompt(
        self,
        user_context: str,
        llm_model: str,
        family: ImageModelFamily,
        style: BannerStyle | None,
    ) -> tuple[str, float]:
        """Use LLM to generate a model-aware image prompt from project context.

        The system prompt is built dynamically based on:
        - family: tells the LLM which prompt rules to follow (DALL-E / Flux / Kontext)
        - style: aesthetic direction hint injected into the system prompt

        Falls back to @balanced on 402, then to a hardcoded generic prompt.

        Returns: (prompt_text, cost_usd)
        """
        sdk = self._get_sdk()
        system_prompt = build_system_prompt(family=family, style=style)

        # Deduplicate: if llm_model is already @balanced, no point retrying it
        fallback_model = "@balanced"
        candidates = [llm_model] if llm_model == fallback_model else [llm_model, fallback_model]

        for model in candidates:
            try:
                @retry(
                    retry=retry_if_exception(_is_transient_llm),
                    stop=stop_after_attempt(3),
                    wait=wait_exponential(multiplier=1, min=2, max=10),
                    reraise=True,
                )
                def _call_llm():
                    return sdk.parse(
                        model=model,
                        messages=[
                            {"role": "system", "content": system_prompt},
                            {"role": "user", "content": user_context},
                        ],
                        response_format=BannerPrompt,
                        max_tokens=1024,
                    )

                result = _call_llm()
                choice = result.choices[0]
                parsed = choice.message.parsed
                # If truncated but parsed partially — try raw content extraction
                if parsed is None and choice.finish_reason == "length":
                    raw_content = getattr(choice.message, "content", None) or ""
                    _, parsed, _ = JSONExtractor.extract(raw_content, BannerPrompt)
                    if parsed:
                        logger.warning("LLM %s: truncated response, recovered via JSONExtractor", model)
                    else:
                        logger.warning("LLM %s: truncated and unrecoverable, using fallback", model)
                        break
                if parsed:
                    prompt_text = parsed.prompt.strip()
                    cost = float(result.usage.total_tokens * 0.000001) if result.usage else 0.0
                    logger.info(
                        "LLM prompt via %s | family=%s style=%s (%d chars)",
                        model, family.value, style.value if style else "auto", len(prompt_text),
                    )
                    return prompt_text, cost
            except Exception as exc:
                is_credits_error = "402" in str(exc) or "insufficient" in str(exc).lower()
                if is_credits_error and model != fallback_model:
                    logger.warning("LLM %s: insufficient credits, retrying %s", model, fallback_model)
                    continue
                if is_credits_error:
                    logger.error("LLM %s: insufficient credits — top up at https://sdkrouter.com", model)
                else:
                    logger.error("LLM prompt enhancement failed (%s): %s", model, exc)
                break

        # Hardcoded fallback — generic but acceptable
        fallback = (
            "Pixar-quality 3D render of a sleek futuristic developer tool interface. "
            "Electric blue and dark navy color palette. Cinematic lighting with neon accent glow. "
            "Clean, modern, professional tech aesthetic. Wide 16:9 composition. "
            "No text, no watermarks, ultra-high quality render."
        )
        logger.warning("Using fallback prompt (LLM failed)")
        return fallback, 0.0


# ── Async resource ────────────────────────────────────────────────────────────

class AsyncBannerResource(AsyncBaseResource):
    """Async banner / hero image generator. Same parameters as BannerResource."""

    def __init__(self, config: "SDKConfig") -> None:
        super().__init__(config)

    async def generate(
        self,
        *,
        source: str | Path | None = None,
        title: str | None = None,
        description: str | None = None,
        github_url: str | None = None,
        prompt: str | None = None,
        preset: BannerStyle | str = DEFAULT_STYLE,
        output: str | Path | None = None,
        size: BannerSize | str = DEFAULT_SIZE,
        model: BannerModel | str = DEFAULT_MODEL,
        quality: BannerQuality | str = DEFAULT_QUALITY,
        style: BannerImageStyle | str = DEFAULT_IMAGE_STYLE,
        llm_model: BannerLLMModel | str = DEFAULT_LLM_MODEL,
        dry_run: bool = False,
    ) -> BannerResult:
        """Async version of BannerResource.generate(). Same parameters and behavior."""
        import asyncio

        loop = asyncio.get_event_loop()
        sync_resource = BannerResource(self._config)
        return await loop.run_in_executor(
            None,
            lambda: sync_resource.generate(
                source=source,
                title=title,
                description=description,
                github_url=github_url,
                prompt=prompt,
                preset=preset,
                output=output,
                size=size,
                model=model,
                quality=quality,
                style=style,
                llm_model=llm_model,
                dry_run=dry_run,
            ),
        )


__all__ = ["BannerResource", "AsyncBannerResource"]
