from typing import Literal

from pydantic import BaseModel, Field


class ChatMessage(BaseModel):
    role: Literal["user", "assistant"]
    content: str
    timestamp: str | None = None


class TripSpec(BaseModel):
    """Mirrors goplaces_backend/v4/persistence.py's DEFAULT_TRIP_SPEC shape - kept as a
    free-ish dict for budget/travelers since the caller (Meridian-Backend) owns the
    canonical structure and this service shouldn't need to change when that does.
    """

    home_city: str | None = None
    start_date: str | None = None
    end_date: str | None = None
    budget: dict | None = None
    travelers: dict | None = None


class Cost(BaseModel):
    amount: float | None = None
    currency: str | None = None
    is_estimate: bool = True


# ── generate_itinerary ──


class GenerateItineraryRequest(BaseModel):
    trip_brief: str
    trip_spec: TripSpec | None = None
    preferences: dict | None = None
    answered_questions: list[dict] = Field(default_factory=list)
    # Optional real inventory (e.g. already fetched via SerpApi by Meridian-Backend) -
    # when present the model is instructed to build around these instead of inventing
    # its own flights/stays. Absent, generation still works, just with LLM-estimated
    # pricing (flagged via Cost.is_estimate).
    flight_candidates: list[dict] = Field(default_factory=list)
    stay_candidates: list[dict] = Field(default_factory=list)
    event_candidates: list[dict] = Field(default_factory=list)
    num_options: int = 3
    # Provider + model override — provider selects the LLM service (gemini/openai/anthropic/ollama);
    # model overrides the specific variant within that service (e.g. "gpt-4o" instead of "gpt-4o-mini").
    # Both are optional: omitting both uses the configured LLM_PROVIDER default.
    provider: str | None = None
    model: str | None = None


class ItineraryDestination(BaseModel):
    activities: list[str] = Field(default_factory=list)
    estimated_cost: Cost | None = None


class ItineraryDay(BaseModel):
    title: str | None = None
    # Day-keyed-by-destination-name shape, same as v4/persistence.py's itinerary_map -
    # proven to be something the prompted model can produce reliably.
    destinations: dict[str, ItineraryDestination] = Field(default_factory=dict)


class SelectedFlight(BaseModel):
    """A flight chosen (or estimated) by the model for one leg of this itinerary option."""
    airline: str | None = None
    flight_number: str | None = None
    departure_airport: str | None = None
    arrival_airport: str | None = None
    departure_datetime: str | None = None  # "YYYY-MM-DD HH:MM" or ISO
    arrival_datetime: str | None = None
    cost: float | None = None
    currency: str | None = None
    booking_url: str | None = None
    is_estimate: bool = True


class SelectedStay(BaseModel):
    """An accommodation chosen (or estimated) by the model for this itinerary option."""
    name: str
    address: str | None = None
    check_in_date: str | None = None   # "YYYY-MM-DD"
    check_out_date: str | None = None
    room_type: str | None = None
    cost_per_night: float | None = None
    currency: str | None = None
    booking_url: str | None = None
    is_estimate: bool = True


class ItineraryOption(BaseModel):
    letter: str
    name: str
    is_ai_pick: bool = False
    summary: str
    total_cost: Cost | None = None
    days: dict[str, ItineraryDay] = Field(default_factory=dict)
    flights: list[SelectedFlight] = Field(default_factory=list)
    stays: list[SelectedStay] = Field(default_factory=list)


class GenerateItineraryResponse(BaseModel):
    options: list[ItineraryOption]
    # Provider metadata — tells the caller which LLM was actually used and
    # which ones were skipped (rate-limited or credit-exhausted), including
    # how many seconds until a rate-limited provider is available again.
    provider_used: str | None = None
    skipped_providers: list[dict] = Field(default_factory=list)


# ── suggest_questions ──


class SuggestQuestionsRequest(BaseModel):
    trip_brief: str
    trip_spec: TripSpec | None = None
    preferences: dict | None = None


class SuggestedQuestion(BaseModel):
    id: str
    text: str
    type: Literal["single_choice", "free_text"] = "single_choice"
    options: list[str] = Field(default_factory=list)


class SuggestQuestionsResponse(BaseModel):
    questions: list[SuggestedQuestion]


# ── suggest_response ──


class SuggestResponseRequest(BaseModel):
    conversation_history: list[ChatMessage]
    trip_context: dict | None = None
    num_suggestions: int = 3


class ResponseSuggestion(BaseModel):
    text: str
    tone: str


class SuggestResponseResponse(BaseModel):
    suggestions: list[ResponseSuggestion]
    context_used: list[str] = Field(default_factory=list)


# ── recommend_stay ──


class RecommendStayRequest(BaseModel):
    itinerary: dict
    trip_spec: TripSpec | None = None
    preferences: dict | None = None
    # Optional real search results (e.g. from a SerpApi/Amadeus call Meridian-Backend
    # already made) - when present the model must choose only from these; when absent
    # it suggests plausible options from general knowledge and every result is flagged
    # source="llm_suggested" so the UI never presents an unverified guess as if it were
    # live availability/pricing.
    candidates: list[dict] = Field(default_factory=list)
    num_recommendations: int = 3


class StayRecommendation(BaseModel):
    name: str
    location: str | None = None
    price_per_night: float | None = None
    currency: str | None = None
    why: str
    is_top_pick: bool = False
    source: Literal["search_candidate", "llm_suggested"]
    source_ref: str | None = None


class RecommendStayResponse(BaseModel):
    recommendations: list[StayRecommendation]
