import json
from dataclasses import asdict, dataclass
from typing import Optional

from dataclass_wizard import JSONWizard
from django import forms
from django.contrib.auth import get_user_model
from django.db.models import Count
from people.models import Person
from scrobbles.forms import form_from_dataclass

User = get_user_model()


class ScrobbleLogDataEncoder(json.JSONEncoder):
    def default(self, o):
        try:
            return o.__dict__
        except AttributeError:
            return {}


class ScrobbleLogDataDecoder(json.JSONDecoder):
    def default(self, o):
        return o.__dict__


class JSONDataclass(JSONWizard):
    @property
    def asdict(self):
        return asdict(self)

    @property
    def json(self):
        return json.dumps(self.asdict)


@dataclass
class BaseLogData(JSONDataclass):
    description: Optional[str] = None
    notes: Optional[list[str]] = None

    _excluded_fields = {}

    @classmethod
    def form(cls):
        return form_from_dataclass(cls)

    @classmethod
    def override_fields(cls) -> dict:
        return {}

    @classmethod
    def from_log_dict(cls, log_dict: dict) -> dict:
        """Extract LogData keyword arguments from a stored log dict.

        Override in subclasses to handle custom nesting/structure.
        """
        return {
            k: v
            for k, v in log_dict.items()
            if k in cls.__dataclass_fields__
        }

    def notes_as_str(self, separator: str = " | ") -> str:
        import html
        import re

        if not self.notes:
            return ""

        if isinstance(self.notes, dict):
            lines = []
            for ts, text in self.notes.items():
                note_text = " ".join(text.strip().split())
                lines.append(f"{ts}: {note_text}")
            return separator.join(lines)

        if isinstance(self.notes, str):
            return html.escape(re.sub(r"\s*\[start:\d+\|end:\d+\]$", "", self.notes))

        if isinstance(self.notes, list):
            cleaned_notes = []
            for note in self.notes:
                if isinstance(note, dict):
                    timestamp, note_text = next(iter(note.items()))
                    note_text = " ".join(note_text.strip().split())
                    cleaned_notes.append(f"{timestamp}: {note_text}")
                elif isinstance(note, str):
                    cleaned = html.escape(
                        re.sub(r"\s*\[start:\d+\|end:\d+\]$", "", note)
                    )
                    cleaned_notes.append(cleaned)
            return separator.join(cleaned_notes)

        return ""

    @staticmethod
    def _format_timestamp(ts: str) -> str:
        from datetime import datetime, timezone
        import re

        cleaned = ts.strip().strip("[]")
        dt = None

        if cleaned.isdigit():
            try:
                seconds = int(cleaned)
                dt = datetime.fromtimestamp(seconds, tz=timezone.utc)
            except (ValueError, OSError):
                pass

        if dt is None:
            for fmt in [
                "%Y-%m-%dT%H:%M:%S.%fZ",
                "%Y-%m-%dT%H:%M:%SZ",
                "%Y-%m-%dT%H:%M:%S.%f",
                "%Y-%m-%dT%H:%M:%S",
            ]:
                try:
                    dt = datetime.strptime(cleaned, fmt)
                    break
                except ValueError:
                    continue

        if dt is None:
            m = re.match(r"(\d{4}-\d{2}-\d{2})\s+\w{3}\s+(\d{2}:\d{2})", cleaned)
            if m:
                try:
                    dt = datetime.strptime(
                        f"{m.group(1)} {m.group(2)}", "%Y-%m-%d %H:%M"
                    )
                except ValueError:
                    pass

        if dt is None:
            try:
                dt = datetime.fromisoformat(cleaned)
            except ValueError:
                pass

        if dt:
            return dt.strftime("%b %-d, %Y %-I:%M %p")
        return ts

    def notes_as_html(self) -> str:
        import bleach
        import markdown
        from django.utils.safestring import mark_safe

        if not self.notes:
            return ""

        md = markdown.Markdown(extensions=["extra"])
        allowed_tags = [
            "p",
            "br",
            "strong",
            "em",
            "a",
            "ul",
            "ol",
            "li",
            "code",
            "pre",
            "blockquote",
            "h1",
            "h2",
            "h3",
            "h4",
            "h5",
            "h6",
            "hr",
            "img",
        ]

        note_items = []

        if isinstance(self.notes, dict):
            for ts, text in sorted(self.notes.items(), reverse=True):
                note_items.append((ts, text.strip()))
        elif isinstance(self.notes, str):
            for line in self.notes.split("\n"):
                if line.strip():
                    note_items.append((None, line.strip()))
        elif isinstance(self.notes, list):
            for note in self.notes:
                if isinstance(note, dict):
                    timestamp, note_text = next(iter(note.items()))
                    note_items.append((timestamp, note_text.strip()))
                elif isinstance(note, str):
                    for line in note.split("\n"):
                        if line.strip():
                            note_items.append((None, line.strip()))

        html_parts = []
        for i, (ts, text) in enumerate(note_items):
            if i > 0:
                html_parts.append('<hr class="note-divider">')

            ts_html = ""
            if ts:
                ts_html = (
                    f'<h5 class="note-timestamp">{self._format_timestamp(ts)}</h5>'
                )

            content_html = bleach.clean(
                md.convert(text),
                tags=allowed_tags,
                strip=True,
            )

            html_parts.append(
                f'<div class="note-item">{ts_html}<div class="note-content">{content_html}</div></div>'
            )

        return mark_safe("".join(html_parts))


@dataclass
class LongPlayLogData(JSONDataclass):
    pass


@dataclass
class SportEventLogData(BaseLogData):
    thesportsdb_id: Optional[str] = None
    start: Optional[str] = None
    round_name: Optional[str] = None
    season_name: Optional[str] = None


@dataclass
class WithPeopleLogData(JSONDataclass):
    with_people_ids: Optional[list[int]] = None

    @property
    def with_people(self) -> list["Person"]:
        from people.models import Person

        if not self.with_people_ids:
            return []
        return [Person.objects.filter(id=pid).first() for pid in self.with_people_ids]

    @classmethod
    def override_fields(cls) -> dict:
        fields = {}
        for base in cls.mro()[1:]:
            if hasattr(base, "override_fields"):
                base_fields = base.override_fields()
                fields.update(base_fields)
        custom_fields = {
            "with_people_ids": forms.ModelMultipleChoiceField(
                queryset=Person.objects.annotate(
                    scrobble_count=Count("scrobble_associations")
                ).order_by("-scrobble_count"),
                required=False,
                widget=forms.SelectMultiple(attrs={"size": 10}),
            )
        }
        fields.update(custom_fields)
        return fields
