from typing import Any, TypedDict

from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.http import HttpRequest, HttpResponse
from django.middleware.csrf import get_token
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils import timezone
from django.utils.safestring import mark_safe

from common.components import (
    A,
    AddForm,
    ButtonGroup,
    Div,
    FormFields,
    Fragment,
    Icon,
    ModuleScript,
    NameWithIcon,
    Node,
    Popover,
    SearchField,
    SessionDeviceSelector,
    SessionTimestampButtons,
    StyledButton,
    TableRow,
    paginated_table_content,
)
from common.layout import NavbarPlaytime, render_page
from games.views.general import model_counts
from common.time import (
    dateformat,
    local_strftime,
    timeformat,
)
from common.utils import paginate, truncate
from games.forms import SessionForm
from games.models import Device, Game, Session


class SessionRowData(TypedDict):
    row_id: str
    hx_trigger: str
    hx_get: str
    hx_select: str
    hx_swap: str
    cell_data: list[Node | str]


def session_row_data(session: Session, device_list, csrf_token: str) -> SessionRowData:
    """Canonical session-list row. Single source of truth shared by
    list_sessions and the htmx finish/reset fragments."""
    row_selector = f"#session-row-{session.pk}"
    end_url = reverse("games:list_sessions_end_session", args=[session.pk])
    reset_url = reverse("games:list_sessions_reset_session_start", args=[session.pk])
    actions = ButtonGroup(
        [
            {
                "href": end_url,
                "hx_get": end_url,
                "hx_target": row_selector,
                "hx_swap": "outerHTML",
                "slot": Icon("end"),
                "title": "Finish session now",
                "color": "green",
            }
            if session.timestamp_end is None
            else {},
            {
                "href": reset_url,
                "hx_get": reset_url,
                "hx_target": row_selector,
                "hx_swap": "outerHTML",
                "hx_confirm": "Reset this session's start time to now?",
                "slot": Icon("reset"),
                "title": "Reset start to now",
                "color": "gray",
            }
            if session.timestamp_end is None
            else {},
            {
                "href": reverse("games:edit_session", args=[session.pk]),
                "slot": Icon("edit"),
                "title": "Edit",
            },
            {
                "href": reverse("games:delete_session", args=[session.pk]),
                "slot": Icon("delete"),
                "title": "Delete",
                "color": "red",
            },
        ]
    )
    return SessionRowData(
        row_id=f"session-row-{session.pk}",
        hx_trigger="device-changed from:body",
        hx_get="",
        hx_select=row_selector,
        hx_swap="outerHTML",
        cell_data=[
            NameWithIcon(session=session),
            f"{local_strftime(session.timestamp_start)}"
            f"{f' — {local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}",
            session.duration_formatted_with_mark(),
            SessionDeviceSelector(session, device_list, csrf_token),
            session.created_at.strftime(dateformat),
            actions,
        ],
    )


def session_row(session: Session, device_list, csrf_token: str) -> Node:
    """The single-session <tr> node, rendered through the same TableRow
    path the list table uses."""
    return TableRow(session_row_data(session, device_list, csrf_token))


@login_required
def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse:
    sessions = Session.objects.order_by("-timestamp_start", "created_at")
    device_list = Device.objects.order_by("name")

    # ── Structured filter (JSON) ──
    filter_json = request.GET.get("filter", "")
    if filter_json:
        from games.filters import parse_session_filter

        session_filter = parse_session_filter(filter_json)
        if session_filter is not None:
            sessions = sessions.filter(session_filter.to_q())
    else:
        # ── Legacy free-text search ──
        search_string = request.GET.get("search_string", search_string)
        if search_string != "":
            sessions = sessions.filter(
                Q(game__name__icontains=search_string)
                | Q(game__name__icontains=search_string)
                | Q(game__platform__name__icontains=search_string)
                | Q(device__name__icontains=search_string)
                | Q(device__type__icontains=search_string)
            )
    try:
        last_session = sessions.latest()
    except Session.DoesNotExist:
        last_session = None
    sessions, page_obj, elided_page_range = paginate(request, sessions)
    csrf_token = get_token(request)

    data = {
        "header_action": Div(
            children=[
                SearchField(search_string=search_string),
                Div(
                    children=[
                        A(
                            href=reverse("games:add_session"),
                        )[
                            StyledButton(
                                icon=True,
                                size="xs",
                            )[Icon("play"), "LOG"]
                        ],
                        A(
                            href=reverse(
                                "games:list_sessions_start_session_from_session",
                                args=[last_session.pk],
                            ),
                            children=Popover(
                                popover_content=last_session.game.name,
                                children=[
                                    StyledButton(
                                        icon=True,
                                        color="gray",
                                        size="xs",
                                        children=[
                                            Icon("play"),
                                            truncate(f"{last_session.game.name}"),
                                        ],
                                    )
                                ],
                            ),
                        )
                        if last_session
                        else "",
                    ]
                ),
            ],
            attributes=[("class", "flex justify-between")],
        ),
        "columns": [
            "Name",
            "Date",
            "Duration",
            "Device",
            "Created",
            "Actions",
        ],
        "rows": [
            session_row_data(session, device_list, csrf_token) for session in sessions
        ],
    }
    content = paginated_table_content(
        data,
        page_obj=page_obj,
        elided_page_range=elided_page_range,
        request=request,
    )
    from common.components import SessionFilterBar

    filter_json = request.GET.get("filter", "")
    filter_bar = SessionFilterBar(
        filter_json=filter_json,
        preset_list_url=reverse("games:list_presets"),
        preset_save_url=reverse("games:save_preset"),
    )
    content = Fragment(filter_bar, content)
    return render_page(
        request,
        content,
        title="Manage sessions",
    )


@login_required
def search_sessions(request: HttpRequest) -> HttpResponse:
    return list_sessions(request, search_string=request.GET.get("search_string", ""))


def _timestamp_buttons(field_name: str) -> Node:
    """The now/toggle/copy helper buttons appended to a timestamp field's row."""
    this_side = "start" if field_name == "timestamp_start" else "end"
    other_side = "end" if field_name == "timestamp_start" else "start"
    return SessionTimestampButtons(
        class_="flex flex-row gap-3 justify-start mt-3",
        hx_boost="false",
    )[
        StyledButton(data_target=field_name, data_type="now", size="xs")["Set to now"],
        StyledButton(data_target=field_name, data_type="toggle", size="xs")[
            "Toggle text"
        ],
        StyledButton(data_target=field_name, data_type="copy", size="xs")[
            f"Copy {this_side} value to {other_side}"
        ],
    ]


def _session_fields(form) -> Node:
    """Session form fields via the shared renderer, with timestamp helper
    buttons appended to the two timestamp rows."""
    return FormFields(
        form,
        extras={
            name: _timestamp_buttons(name)
            for name in ("timestamp_start", "timestamp_end")
        },
    )


@login_required
def add_session(request: HttpRequest, game_id: int = 0) -> HttpResponse:
    initial: dict[str, Any] = {"timestamp_start": timezone.now()}

    if request.method == "POST":
        form = SessionForm(request.POST or None, initial=initial)
        if form.is_valid():
            form.save()
            return redirect("games:list_sessions")
    else:
        if game_id:
            game = get_object_or_404(Game, id=game_id)
            form = SessionForm(
                initial={
                    **initial,
                    "game": game,
                }
            )
        else:
            form = SessionForm(initial=initial)

    # TODO: re-add custom buttons #91
    return render_page(
        request,
        AddForm(form, request=request, fields=_session_fields(form), submit_class=""),
        title="Add New Session",
        scripts=mark_safe(ModuleScript("dist/elements/search-select.js")),
    )


@login_required
def edit_session(request: HttpRequest, session_id: int) -> HttpResponse:
    session = get_object_or_404(Session, id=session_id)
    form = SessionForm(request.POST or None, instance=session)
    if form.is_valid():
        form.save()
        return redirect("games:list_sessions")
    return render_page(
        request,
        AddForm(form, request=request, fields=_session_fields(form), submit_class=""),
        title="Edit Session",
        scripts=mark_safe(ModuleScript("dist/elements/search-select.js")),
    )


def _row_with_navbar(request: HttpRequest, session: Session) -> HttpResponse:
    device_list = Device.objects.order_by("name")
    counts = model_counts(request)
    fragment = Fragment(
        session_row(session, device_list, get_token(request)),
        NavbarPlaytime(
            counts["today_played"],
            counts["last_7_played"],
            today_url=counts["today_url"],
            last_7_url=counts["last_7_url"],
            oob=True,
        ),
    )
    return HttpResponse(str(fragment))


def clone_session_by_id(session_id: int) -> Session:
    session = get_object_or_404(Session, id=session_id)
    clone = session
    clone.pk = None
    clone.timestamp_start = timezone.now()
    clone.timestamp_end = None
    clone.note = ""
    clone.save()
    return clone


@login_required
def new_session_from_existing_session(
    request: HttpRequest, session_id: int
) -> HttpResponse:
    clone_session_by_id(session_id)
    if request.htmx:
        # Clone adds a new row whose position depends on sort + pagination,
        # which a single-row swap cannot place — refresh the list instead.
        response = HttpResponse(status=204)
        response["HX-Refresh"] = "true"
        return response
    return redirect("games:list_sessions")


@login_required
def end_session(request: HttpRequest, session_id: int) -> HttpResponse:
    session = get_object_or_404(Session, id=session_id)
    session.timestamp_end = timezone.now()
    session.save()
    if request.htmx:
        return _row_with_navbar(request, session)
    return redirect("games:list_sessions")


@login_required
def reset_session_start(request: HttpRequest, session_id: int) -> HttpResponse:
    session = get_object_or_404(Session, id=session_id)
    session.timestamp_start = timezone.now()
    session.save()
    if request.htmx:
        return _row_with_navbar(request, session)
    return redirect("games:list_sessions")


@login_required
def delete_session(request: HttpRequest, session_id: int = 0) -> HttpResponse:
    session = get_object_or_404(Session, id=session_id)
    session.delete()
    return redirect("games:list_sessions")
