from __future__ import annotations


from sqlalchemy import select, update

from models.calendars import CalendarEntry, CalendarSlot

from shared.browser.app.authz import require_admin
from shared.browser.app.redis_cacher import clear_cache


from sqlalchemy import select
from quart import (
    request, make_response, Blueprint, g, jsonify
)
from ..calendar_entries.services.entries import (
    svc_update_entry,
    CalendarError,   # <-- add this if you want to catch it explicitly
)
from .services.post_associations import (
    add_post_to_entry,
    remove_post_from_entry,
    get_entry_posts,
    search_posts as svc_search_posts,
)
from datetime import datetime, timezone
import math
import logging

from shared.infrastructure.fragments import fetch_fragment
from shared.sx.helpers import sx_response

from ..ticket_types.routes import register as register_ticket_types

from .admin.routes import register as register_admin


logger = logging.getLogger(__name__)

def register():
    bp = Blueprint("calendar_entry", __name__, url_prefix='/<int:entry_id>')

    # Register tickets blueprint
    bp.register_blueprint(
        register_ticket_types()
    )
    bp.register_blueprint(
        register_admin()
    )

    @bp.before_request
    async def load_entry():
        """Load the calendar entry from the URL parameter."""
        from quart import abort
        entry_id = request.view_args.get("entry_id")
        if entry_id:
            result = await g.s.execute(
                select(CalendarEntry)
                .where(
                    CalendarEntry.id == entry_id,
                    CalendarEntry.deleted_at.is_(None)
                )
            )
            g.entry = result.scalar_one_or_none()
            if g.entry is None:
                abort(404)

    @bp.context_processor
    async def inject_entry():
        """Make entry and date parameters available to all templates in this blueprint."""
        return {
            "entry": getattr(g, "entry", None),
            "year": request.view_args.get("year"),
            "month": request.view_args.get("month"),
            "day": request.view_args.get("day"),
        }

    async def get_day_nav_oob(year: int, month: int, day: int):
        """Helper to generate OOB update for day entries nav"""
        from datetime import datetime, timezone, date, timedelta
        from ..calendar.services import get_visible_entries_for_period
        from quart import session as qsession

        # Get the calendar from g
        calendar = getattr(g, "calendar", None)
        if not calendar:
            return ""

        # Build day date
        try:
            day_date = date(year, month, day)
        except (ValueError, TypeError):
            return ""

        # Period: this day only
        period_start = datetime(year, month, day, tzinfo=timezone.utc)
        period_end = period_start + timedelta(days=1)

        # Identity
        user = getattr(g, "user", None)
        session_id = qsession.get("calendar_sid")

        # Get confirmed entries for this day
        visible = await get_visible_entries_for_period(
            sess=g.s,
            calendar_id=calendar.id,
            period_start=period_start,
            period_end=period_end,
            user=user,
            session_id=session_id,
        )

        # Render OOB nav
        from sxc.pages.entries import render_day_entries_nav_oob
        return render_day_entries_nav_oob(visible.confirmed_entries, calendar, day_date)

    async def get_post_nav_oob(entry_id: int):
        """Helper to generate OOB update for post entries nav when entry state changes"""
        # Get the entry to find associated posts
        entry = await g.s.scalar(
            select(CalendarEntry).where(
                CalendarEntry.id == entry_id,
                CalendarEntry.deleted_at.is_(None)
            )
        )
        if not entry:
            return ""

        # Get all posts associated with this entry
        from .services.post_associations import get_entry_posts
        entry_posts = await get_entry_posts(g.s, entry_id)

        # Generate OOB updates for each post's nav
        nav_oobs = []
        for post in entry_posts:
            # Get associated entries for this post
            from shared.services.entry_associations import get_associated_entries
            associated_entries = await get_associated_entries(post.id)

            # Load calendars for this post
            from models.calendars import Calendar
            calendars = (
                await g.s.execute(
                    select(Calendar)
                    .where(Calendar.container_type == "page", Calendar.container_id == post.id, Calendar.deleted_at.is_(None))
                    .order_by(Calendar.name.asc())
                )
            ).scalars().all()

            # Render OOB nav for this post
            from sxc.pages.entries import render_post_nav_entries_oob
            nav_oob = render_post_nav_entries_oob(associated_entries, calendars, post)
            nav_oobs.append(nav_oob)

        return "".join(nav_oobs)

    @bp.context_processor
    async def inject_root():
        from ..tickets.services.tickets import (
            get_available_ticket_count,
            get_sold_ticket_count,
            get_user_reserved_count,
        )
        from shared.infrastructure.cart_identity import current_cart_identity
        from sqlalchemy.orm import selectinload

        view_args = getattr(request, "view_args", {}) or {}
        entry_id = view_args.get("entry_id")
        calendar_entry = None
        entry_posts = []
        ticket_remaining = None
        ticket_sold_count = 0
        user_ticket_count = 0
        user_ticket_counts_by_type = {}

        stmt = (
            select(CalendarEntry)
            .where(
                CalendarEntry.id == entry_id,
                CalendarEntry.deleted_at.is_(None),
            )
            .options(selectinload(CalendarEntry.ticket_types))
        )
        result = await g.s.execute(stmt)
        calendar_entry = result.scalar_one_or_none()

        # Optional: also ensure it belongs to the current calendar, if g.calendar is set
        if calendar_entry is not None and getattr(g, "calendar", None):
            if calendar_entry.calendar_id != g.calendar.id:
                calendar_entry = None

        # Refresh slot relationship if we have a valid entry
        if calendar_entry is not None:
            await g.s.refresh(calendar_entry, ['slot'])
            # Fetch associated posts
            entry_posts = await get_entry_posts(g.s, calendar_entry.id)
            # Get ticket availability
            ticket_remaining = await get_available_ticket_count(g.s, calendar_entry.id)
            # Get sold count
            ticket_sold_count = await get_sold_ticket_count(g.s, calendar_entry.id)
            # Get current user's reserved count
            ident = current_cart_identity()
            user_ticket_count = await get_user_reserved_count(
                g.s, calendar_entry.id,
                user_id=ident["user_id"],
                session_id=ident["session_id"],
            )
            # Per-type counts for multi-type entries
            if calendar_entry.ticket_types:
                for tt in calendar_entry.ticket_types:
                    if tt.deleted_at is None:
                        user_ticket_counts_by_type[tt.id] = await get_user_reserved_count(
                            g.s, calendar_entry.id,
                            user_id=ident["user_id"],
                            session_id=ident["session_id"],
                            ticket_type_id=tt.id,
                        )

        # Fetch container nav from relations (exclude calendar — we're on a calendar page)
        container_nav = ""
        post_data = getattr(g, "post_data", None)
        if post_data:
            post_id = post_data["post"]["id"]
            post_slug = post_data["post"]["slug"]
            container_nav = await fetch_fragment("relations", "container-nav", params={
                "container_type": "page",
                "container_id": str(post_id),
                "post_slug": post_slug,
                "exclude": "page->calendar",
            })

        return {
            "entry": calendar_entry,
            "entry_posts": entry_posts,
            "ticket_remaining": ticket_remaining,
            "ticket_sold_count": ticket_sold_count,
            "user_ticket_count": user_ticket_count,
            "user_ticket_counts_by_type": user_ticket_counts_by_type,
            "container_nav": container_nav,
        }
    @bp.get("/edit/")
    @require_admin
    async def get_edit(entry_id: int, **rest):
        from datetime import date as date_cls
        va = request.view_args or {}
        day, month, year = va.get("day"), va.get("month"), va.get("year")

        # Query day slots for this weekday
        day_date = date_cls(year, month, day)
        weekday_attr = ["mon","tue","wed","thu","fri","sat","sun"][day_date.weekday()]
        stmt = select(CalendarSlot).where(
            CalendarSlot.calendar_id == g.calendar.id,
            getattr(CalendarSlot, weekday_attr) == True,
            CalendarSlot.deleted_at.is_(None),
        ).order_by(CalendarSlot.time_start.asc(), CalendarSlot.id.asc())
        result = await g.s.execute(stmt)
        day_slots = list(result.scalars())

        from sxc.pages.entries import render_entry_edit_form
        return sx_response(render_entry_edit_form(g.entry, g.calendar, day, month, year, day_slots))

    @bp.put("/")
    @require_admin
    @clear_cache(tag="calendars", tag_scope="all")
    async def put(year: int, month: int, day: int, entry_id: int, **rest):
        form = await request.form

        def parse_time_to_dt(value: str | None, year: int, month: int, day: int):
            """
            'HH:MM' + (year, month, day) -> aware datetime in UTC.
            Returns None if empty/invalid.
            """
            if not value:
                return None
            try:
                hour_str, minute_str = value.split(":", 1)
                hour = int(hour_str)
                minute = int(minute_str)
                return datetime(year, month, day, hour, minute, tzinfo=timezone.utc)
            except Exception:
                return None

        name = (form.get("name") or "").strip()
        start_at = parse_time_to_dt(form.get("start_at"), year, month, day)
        end_at   = parse_time_to_dt(form.get("end_at"),   year, month, day)

        # NEW: slot_id
        slot_id_raw = (form.get("slot_id") or "").strip()
        slot_id = int(slot_id_raw) if slot_id_raw else None

        # Ticket configuration
        ticket_price_str = (form.get("ticket_price") or "").strip()
        ticket_price = None
        if ticket_price_str:
            try:
                from decimal import Decimal
                ticket_price = Decimal(ticket_price_str)
            except Exception:
                pass  # Will be validated below if needed

        ticket_count_str = (form.get("ticket_count") or "").strip()
        ticket_count = None
        if ticket_count_str:
            try:
                ticket_count = int(ticket_count_str)
            except Exception:
                pass  # Will be validated below if needed

        field_errors: dict[str, list[str]] = {}

        # --- Basic validation (slot-style) -------------------------

        if not name:
            field_errors.setdefault("name", []).append(
                "Please enter a name for the entry."
            )

        # Check slot first before validating times
        slot = None
        if slot_id is not None:
            result = await g.s.execute(
                select(CalendarSlot).where(
                    CalendarSlot.id == slot_id,
                    CalendarSlot.calendar_id == g.calendar.id,
                    CalendarSlot.deleted_at.is_(None),
                )
            )
            slot = result.scalar_one_or_none()
            if slot is None:
                field_errors.setdefault("slot_id", []).append(
                    "Selected slot is no longer available."
                )
            else:
                # For inflexible slots, override the times with slot times
                if not slot.flexible:
                    # Replace start/end with slot times
                    start_at = datetime(year, month, day, 
                                       slot.time_start.hour, 
                                       slot.time_start.minute, 
                                       tzinfo=timezone.utc)
                    if slot.time_end:
                        end_at = datetime(year, month, day,
                                         slot.time_end.hour,
                                         slot.time_end.minute,
                                         tzinfo=timezone.utc)
                else:
                    # Flexible: validate times are within slot band
                    # Only validate if times were provided
                    if not start_at:
                        field_errors.setdefault("start_at", []).append(
                            "Please select a start time."
                        )
                    if not end_at:
                        field_errors.setdefault("end_at", []).append(
                            "Please select an end time."
                        )
                    
                    if start_at and end_at:
                        s_time = start_at.timetz()
                        e_time = end_at.timetz()
                        slot_start = slot.time_start
                        slot_end = slot.time_end

                        if s_time.replace(tzinfo=None) < slot_start:
                            field_errors.setdefault("start_at", []).append(
                                f"Start time must be at or after {slot_start.strftime('%H:%M')}."
                            )
                        if slot_end is not None and e_time.replace(tzinfo=None) > slot_end:
                            field_errors.setdefault("end_at", []).append(
                                f"End time must be at or before {slot_end.strftime('%H:%M')}."
                            )
        else:
            field_errors.setdefault("slot_id", []).append(
                "Please select a slot."
            )

        # Time ordering check (only if we have times and no slot override)
        if start_at and end_at and end_at < start_at:
            field_errors.setdefault("end_at", []).append(
                "End time must be after the start time."
            )

        if field_errors:
            return jsonify(
                {
                    "message": "Please fix the highlighted fields.",
                    "errors": field_errors,
                }
            ), 422

        # --- Service call & safety net for extra validation -------

        try:
            entry = await svc_update_entry(
                g.s,
                entry_id,
                name=name,
                start_at=start_at,
                end_at=end_at,
                slot_id=slot_id,  # Pass slot_id to service
            )

            # Update ticket configuration
            entry.ticket_price = ticket_price
            entry.ticket_count = ticket_count

        except CalendarError as e:
            # If the service still finds something wrong, surface it nicely.
            msg = str(e)
            return jsonify(
                {
                    "message": "There was a problem updating the entry.",
                    "errors": {"__all__": [msg]},
                }
            ), 422

        # --- Success: re-render the entry block -------------------

        # Get nav OOB update
        nav_oob = await get_day_nav_oob(year, month, day)

        from shared.sx.page import get_template_context
        from sxc.pages.entries import _entry_main_panel_html

        tctx = await get_template_context()
        html = _entry_main_panel_html(tctx)
        return sx_response(html + nav_oob)


    @bp.post("/confirm/")
    @require_admin
    @clear_cache(tag="calendars", tag_scope="all")
    async def confirm_entry(entry_id: int, year: int, month: int, day: int, **rest):
        await g.s.execute(
            update(CalendarEntry)
            .where(
                CalendarEntry.id == entry_id,
                CalendarEntry.deleted_at.is_(None),
                CalendarEntry.state == "provisional",
            )
            .values(state="confirmed")
        )
        await g.s.flush()

        # Get nav OOB updates (both day and post navs)
        day_nav_oob = await get_day_nav_oob(year, month, day)
        post_nav_oob = await get_post_nav_oob(entry_id)

        # Re-read entry to get updated state
        await g.s.refresh(g.entry)
        from sxc.pages.entries import render_entry_optioned
        html = render_entry_optioned(g.entry, g.calendar, day, month, year)
        return sx_response(html + day_nav_oob + post_nav_oob)

    @bp.post("/decline/")
    @require_admin
    @clear_cache(tag="calendars", tag_scope="all")
    async def decline_entry(entry_id: int, year: int, month: int, day: int, **rest):
        await g.s.execute(
            update(CalendarEntry)
            .where(
                CalendarEntry.id == entry_id,
                CalendarEntry.deleted_at.is_(None),
                CalendarEntry.state == "provisional",
            )
            .values(state="declined")
        )
        await g.s.flush()

        # Get nav OOB updates (both day and post navs)
        day_nav_oob = await get_day_nav_oob(year, month, day)
        post_nav_oob = await get_post_nav_oob(entry_id)

        # Re-read entry to get updated state
        await g.s.refresh(g.entry)
        from sxc.pages.entries import render_entry_optioned
        html = render_entry_optioned(g.entry, g.calendar, day, month, year)
        return sx_response(html + day_nav_oob + post_nav_oob)

    @bp.post("/provisional/")
    @require_admin
    @clear_cache(tag="calendars", tag_scope="all")
    async def provisional_entry(entry_id: int, year: int, month: int, day: int, **rest):
        await g.s.execute(
            update(CalendarEntry)
            .where(
                CalendarEntry.id == entry_id,
                CalendarEntry.deleted_at.is_(None),
                CalendarEntry.state == "confirmed",
            )
            .values(state="provisional")
        )
        await g.s.flush()

        # Get nav OOB updates (both day and post navs)
        day_nav_oob = await get_day_nav_oob(year, month, day)
        post_nav_oob = await get_post_nav_oob(entry_id)

        # Re-read entry to get updated state
        await g.s.refresh(g.entry)
        from sxc.pages.entries import render_entry_optioned
        html = render_entry_optioned(g.entry, g.calendar, day, month, year)
        return sx_response(html + day_nav_oob + post_nav_oob)

    @bp.post("/tickets/")
    @require_admin
    @clear_cache(tag="calendars", tag_scope="all")
    async def update_tickets(entry_id: int, **rest):
        """Update ticket configuration for a calendar entry"""
        from .services.ticket_operations import update_ticket_config
        from decimal import Decimal

        form = await request.form

        # Parse ticket price
        ticket_price_str = (form.get("ticket_price") or "").strip()
        ticket_price = None
        if ticket_price_str:
            try:
                ticket_price = Decimal(ticket_price_str)
            except Exception:
                return await make_response("Invalid ticket price", 400)

        # Parse ticket count
        ticket_count_str = (form.get("ticket_count") or "").strip()
        ticket_count = None
        if ticket_count_str:
            try:
                ticket_count = int(ticket_count_str)
            except Exception:
                return await make_response("Invalid ticket count", 400)

        # Update ticket configuration
        success, error = await update_ticket_config(
            g.s, entry_id, ticket_price, ticket_count
        )

        if not success:
            return await make_response(error, 400)

        await g.s.flush()

        # Return just the tickets fragment (targeted by hx-target="#entry-tickets-...")
        await g.s.refresh(g.entry)
        from sxc.pages.entries import render_entry_tickets_config
        html = render_entry_tickets_config(g.entry, g.calendar, request.view_args.get("day"), request.view_args.get("month"), request.view_args.get("year"))
        return sx_response(html)

    @bp.get("/posts/search/")
    @require_admin
    async def search_posts(entry_id: int, **rest):
        """Search for posts to associate with this entry"""
        query = request.args.get("q", "").strip()
        page = int(request.args.get("page", 1))
        per_page = 10

        search_posts, total = await svc_search_posts(g.s, query, page, per_page)
        total_pages = math.ceil(total / per_page) if total > 0 else 0

        va = request.view_args or {}
        from sxc.pages.entries import render_post_search_results
        return sx_response(render_post_search_results(
            search_posts, query, page, total_pages,
            g.entry, g.calendar,
            va.get("day"), va.get("month"), va.get("year"),
        ))

    @bp.post("/posts/")
    @require_admin
    @clear_cache(tag="calendars", tag_scope="all")
    async def add_post(entry_id: int, **rest):
        """Add a post association to this entry"""
        form = await request.form
        post_id = form.get("post_id")

        if not post_id:
            return await make_response("Post ID is required", 400)

        try:
            post_id = int(post_id)
        except ValueError:
            return await make_response("Invalid post ID", 400)

        success, error = await add_post_to_entry(g.s, entry_id, post_id)

        if not success:
            return await make_response(error, 400)

        await g.s.flush()

        # Reload entry_posts for nav update
        entry_posts = await get_entry_posts(g.s, entry_id)

        # Return updated posts list + OOB nav update
        from sxc.pages.entries import render_entry_posts_panel, render_entry_posts_nav_oob
        va = request.view_args or {}
        html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
        nav_oob = render_entry_posts_nav_oob(entry_posts)
        return sx_response(html + nav_oob)

    @bp.delete("/posts/<int:post_id>/")
    @require_admin
    @clear_cache(tag="calendars", tag_scope="all")
    async def remove_post(entry_id: int, post_id: int, **rest):
        """Remove a post association from this entry"""
        success, error = await remove_post_from_entry(g.s, entry_id, post_id)

        if not success:
            return await make_response(error or "Association not found", 404)

        await g.s.flush()

        # Reload entry_posts for nav update
        entry_posts = await get_entry_posts(g.s, entry_id)

        # Return updated posts list + OOB nav update
        from sxc.pages.entries import render_entry_posts_panel, render_entry_posts_nav_oob
        va = request.view_args or {}
        html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
        nav_oob = render_entry_posts_nav_oob(entry_posts)
        return sx_response(html + nav_oob)

    return bp
