import logging
from dataclasses import dataclass
from html import escape
from typing import Optional

import bleach
import markdown
from django import forms
from django.conf import settings
from django.db import models
from django.urls import reverse
from scrobbles.dataclasses import BaseLogData
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin

logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}

AGENT_HTML_ALLOWED_TAGS = [
    "p",
    "br",
    "strong",
    "em",
    "a",
    "ul",
    "ol",
    "li",
    "code",
    "pre",
    "blockquote",
    "h1",
    "h2",
    "h3",
    "h4",
    "h5",
    "h6",
    "hr",
    "img",
    "table",
    "thead",
    "tbody",
    "tr",
    "th",
    "td",
]


@dataclass
class AgentSessionLogData(BaseLogData):
    title: Optional[str] = None
    provider: Optional[str] = None
    model: Optional[str] = None
    turns: Optional[list] = None

    _excluded_fields = {"turns"}

    @classmethod
    def override_fields(cls) -> dict:
        return {
            "title": forms.CharField(required=False),
            "provider": forms.CharField(required=False),
            "model": forms.CharField(required=False),
        }

    @property
    def pending(self) -> bool:
        if not self.turns:
            return False
        return any(turn.get("response") is None for turn in self.turns)

    @staticmethod
    def _md_to_html(text: str) -> str:
        md = markdown.Markdown(extensions=["extra"])
        return bleach.clean(md.convert(text), tags=AGENT_HTML_ALLOWED_TAGS, strip=True)

    def as_html(self, scrobble_id: int | None = None) -> str:
        if not self.turns:
            return ""

        html_parts = []
        for i, turn in enumerate(self.turns):
            prompt = turn.get("prompt", "")
            response = turn.get("response")
            prompt_id = turn.get("prompt_id", "")
            escaped_prompt_id = escape(prompt_id)
            html_parts.append('<div class="agent-turn">')
            html_parts.append(
                f'<div class="agent-prompt"><h5>Prompt {i + 1}</h5>'
                f"{self._md_to_html(prompt)}</div>"
            )
            if response:
                html_parts.append(
                    f'<div class="agent-response" id="response-{escaped_prompt_id}">'
                    f"<h5>Response</h5>"
                    f"{self._md_to_html(response)}</div>"
                )
                if turn.get("error") and scrobble_id:
                    retry_url = reverse(
                        "scrobbles:agent-session-retry", args=[scrobble_id]
                    )
                    html_parts.append(
                        f'<form class="mt-1" hx-post="{retry_url}" '
                        f'hx-target="#agent-session-content" hx-swap="outerHTML">'
                        f'<input type="hidden" name="prompt_id" '
                        f'value="{escaped_prompt_id}">'
                        f'<button type="submit" '
                        f'class="btn btn-sm btn-outline-warning">Retry</button>'
                        f"</form>"
                    )
            elif turn.get("retryable"):
                attempts = int(turn.get("attempts", 0))
                max_attempts = settings.AGENT_PROVIDER_MAX_ATTEMPTS
                html_parts.append(
                    f'<div class="agent-response agent-thinking" '
                    f'id="response-{escaped_prompt_id}">Retrying&hellip; '
                    f"(attempt {attempts} of {max_attempts})</div>"
                )
            else:
                html_parts.append(
                    f'<div class="agent-response agent-thinking" '
                    f'id="response-{escaped_prompt_id}">Thinking&hellip;</div>'
                )
            html_parts.append("</div>")
        return "".join(html_parts)


class AgentSession(ScrobblableMixin):
    provider = models.CharField(max_length=50, **BNULL)
    model = models.CharField(max_length=100, **BNULL)

    @classmethod
    def find_or_create(cls, provider: str, model: str):
        agent_session = cls.objects.filter(provider=provider, model=model).first()
        if agent_session:
            return agent_session, False
        title = f"{provider} {model}".strip()
        agent_session = cls.objects.create(provider=provider, model=model, title=title)
        return agent_session, True

    def __str__(self):
        if self.title:
            return self.title
        return str(self.uuid)

    def get_absolute_url(self):
        return reverse("agents:agent_session_detail", kwargs={"slug": self.uuid})

    @property
    def subtitle(self) -> str:
        return " ".join(p for p in [self.provider, self.model] if p)

    @property
    def strings(self) -> ScrobblableConstants:
        return ScrobblableConstants(verb="Asking", tags="agents")

    @property
    def start_url(self) -> str:
        return ""

    @property
    def logdata_cls(self):
        return AgentSessionLogData

    @property
    def primary_image_url(self) -> str:
        return ""
