from __future__ import annotations
from datetime import datetime
from typing import Optional, Sequence
from decimal import Decimal

from sqlalchemy import select, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession

from models.calendars import Calendar, CalendarEntry

from datetime import datetime

from shared.browser.app.errors import AppError

class CalendarError(AppError):
    """Base error for calendar service operations."""
    status_code = 422



async def add_entry(
    sess: AsyncSession,
    calendar_id: int,
    name: str,
    start_at: Optional[datetime],
    end_at: Optional[datetime],
    user_id: int | None = None,
    session_id: str | None = None,
    slot_id: int | None = None,  # NEW: accept slot_id
    cost: Optional[Decimal] = None,  # NEW: accept cost
) -> CalendarEntry:
    """
    Add an entry to a calendar.

    Collects *all* validation errors and raises CalendarError([...])
    so the HTMX handler can show them as a list.
    """
    errors: list[str] = []

    # Normalise
    name = (name or "").strip()

    # Name validation
    if not name:
        errors.append("Entry name must not be empty.")

    # start_at validation
    if start_at is None:
        errors.append("Start time is required.")
    elif not isinstance(start_at, datetime):
        errors.append("Start time is invalid.")

    # end_at validation
    if end_at is not None and not isinstance(end_at, datetime):
        errors.append("End time is invalid.")

    # Time ordering (only if we have sensible datetimes)
    if isinstance(start_at, datetime) and isinstance(end_at, datetime):
        if end_at < start_at:
            errors.append("End time must be greater than or equal to the start time.")

    # If we have any validation errors, bail out now
    if errors:
        raise CalendarError(errors, status_code=422)

    # Calendar existence (this is more of a 404 than a validation issue)
    cal = (
        await sess.execute(
            select(Calendar).where(
                Calendar.id == calendar_id,
                Calendar.deleted_at.is_(None),
            )
        )
    ).scalar_one_or_none()

    if not cal:
        # Single-message CalendarError – still handled by the same error handler
        raise CalendarError(
            f"Calendar {calendar_id} does not exist or has been deleted.",
            status_code=404,
        )

    # All good, create the entry
    entry = CalendarEntry(
        calendar_id=calendar_id,
        name=name,
        start_at=start_at,
        end_at=end_at,
        user_id=user_id,
        session_id=session_id,
        slot_id=slot_id,  # NEW: save slot_id
        state="pending",
        cost=cost if cost is not None else Decimal('10'),  # Use provided cost or default
    )
    sess.add(entry)
    await sess.flush()

    # Publish to federation inline
    if entry.user_id:
        from shared.services.federation_publish import try_publish
        await try_publish(
            sess,
            user_id=entry.user_id,
            activity_type="Create",
            object_type="Event",
            object_data={
                "name": entry.name or "",
                "startTime": entry.start_at.isoformat() if entry.start_at else "",
                "endTime": entry.end_at.isoformat() if entry.end_at else "",
            },
            source_type="CalendarEntry",
            source_id=entry.id,
        )

    return entry


async def list_entries(
    sess: AsyncSession,
    post_id: int,
    calendar_slug: str,
    from_: Optional[datetime] = None,
    to: Optional[datetime] = None,
) -> Sequence[CalendarEntry]:
    """
    List entries for a given post's calendar by name.
    - Respects soft-deletes (only non-deleted calendar / entries).
    - If a time window is provided, returns entries that overlap the window:
        - If only from_ is given: entries where end_at is NULL or end_at >= from_
        - If only to is given:   entries where start_at <= to
        - If both given:         entries where [start_at, end_at or +inf] overlaps [from_, to]
    - Sorted by start_at ascending.
    """
    calendar_slug = (calendar_slug or "").strip()
    if not calendar_slug:
        raise CalendarError("calendar_slug must not be empty.")

    cal = (
        await sess.execute(
            select(Calendar.id)
            .where(
                Calendar.container_type == "page",
                Calendar.container_id == post_id,
                Calendar.slug == calendar_slug,
                Calendar.deleted_at.is_(None),
            )
        )
    ).scalar_one_or_none()

    if not cal:
        # Return empty list instead of raising, so callers can treat absence as "no entries"
        return []

    # Base filter: not soft-deleted entries of this calendar
    filters = [CalendarEntry.calendar_id == cal, CalendarEntry.deleted_at.is_(None)]

    # Time window logic
    if from_ and to:
        # Overlap condition: start <= to AND (end is NULL OR end >= from_)
        filters.append(CalendarEntry.start_at <= to)
        filters.append(or_(CalendarEntry.end_at.is_(None), CalendarEntry.end_at >= from_))
    elif from_:
        # Anything that hasn't ended before from_
        filters.append(or_(CalendarEntry.end_at.is_(None), CalendarEntry.end_at >= from_))
    elif to:
        # Anything that has started by 'to'
        filters.append(CalendarEntry.start_at <= to)

    stmt = (
        select(CalendarEntry)
        .where(and_(*filters))
        .order_by(CalendarEntry.start_at.asc(), CalendarEntry.id.asc())
    )

    result = await sess.execute(stmt)
    entries = list(result.scalars())
    
    # Eagerly load slot relationships
    for entry in entries:
        await sess.refresh(entry, ['slot'])
    
    return entries


async def svc_update_entry(
    sess: AsyncSession,
    entry_id: int,
    *,
    name: str | None = None,
    start_at: datetime | None = None,
    end_at: datetime | None = None,
    user_id: int | None = None,
    session_id: str | None = None,
    slot_id: int | None = None,  # NEW: accept slot_id
    cost: Decimal | None = None,  # NEW: accept cost
) -> CalendarEntry:
    """
    Update an existing CalendarEntry.
    
    - Performs the same validations as add_entry()
    - Returns the updated CalendarEntry
    - Raises CalendarError([...]) on validation issues
    - Raises CalendarError(...) if entry does not exist
    """

    # Fetch entry
    entry = (
        await sess.execute(
            select(CalendarEntry).where(
                CalendarEntry.id == entry_id,
                CalendarEntry.deleted_at.is_(None),
            )
        )
    ).scalar_one_or_none()

    if not entry:
        raise CalendarError(
            f"Entry {entry_id} does not exist or has been deleted.",
            status_code=404,
        )

    errors: list[str] = []

    # ----- Validation ----- #

    # Name validation only if updating it
    if name is not None:
        name = name.strip()
        if not name:
            errors.append("Entry name must not be empty.")

    # start_at type validation only if provided
    if start_at is not None and not isinstance(start_at, datetime):
        errors.append("Start time is invalid.")

    # end_at type validation
    if end_at is not None and not isinstance(end_at, datetime):
        errors.append("End time is invalid.")

    # Time ordering
    effective_start = start_at if start_at is not None else entry.start_at
    effective_end = end_at if end_at is not None else entry.end_at

    if isinstance(effective_start, datetime) and isinstance(effective_end, datetime):
        if effective_end < effective_start:
            errors.append("End time must be greater than or equal to the start time.")

    # Validation failures?
    if errors:
        raise CalendarError(errors, status_code=422)

    # ----- Apply Updates ----- #

    if name is not None:
        entry.name = name

    if start_at is not None:
        entry.start_at = start_at

    if end_at is not None:
        entry.end_at = end_at

    if user_id is not None:
        entry.user_id = user_id

    if session_id is not None:
        entry.session_id = session_id

    if slot_id is not None:  # NEW: update slot_id
        entry.slot_id = slot_id
    
    if cost is not None:  # NEW: update cost
        entry.cost = cost

    entry.updated_at = datetime.utcnow()

    await sess.flush()
    return entry