//! Provider-neutral event types.
//!
//! These types represent calendar events in a provider-agnostic way.
//! Providers convert their API responses into these types, and caldir-cli
//! works exclusively with them for sync, diff, and ICS generation.

use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;

pub use crate::event_time::EventTime;

/// Typed recurrence data for a master event.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recurrence {
    /// RRULE value (after "RRULE:"), e.g. "FREQ=WEEKLY;BYDAY=MO"
    pub rrule: String,
    /// Exception dates (EXDATE)
    pub exdates: Vec<EventTime>,
}

impl PartialEq for Recurrence {
    fn eq(&self, other: &Self) -> bool {
        if self.rrule != other.rrule {
            return false;
        }
        let mut a = self.exdates.clone();
        let mut b = other.exdates.clone();
        a.sort_by_key(|x| x.to_utc());
        b.sort_by_key(|x| x.to_utc());
        a == b
    }
}

/// A calendar event (provider-neutral)
///
/// `PartialEq` compares content fields only, ignoring sync metadata
/// (`updated`, `sequence`, `custom_properties`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
    /// RFC 5545 UID - unique identifier shared across recurring event instances.
    /// Always present. For recurring events, the master and all instance overrides
    /// share the same uid, linked via recurrence_id.
    pub uid: String,
    pub summary: String,
    pub description: Option<String>,
    pub location: Option<String>,
    pub start: EventTime,
    pub end: EventTime,
    pub status: EventStatus,

    // Recurrence fields
    /// Typed recurrence data (RRULE + EXDATEs) for master events
    pub recurrence: Option<Recurrence>,
    /// RECURRENCE-ID: Original start time for this instance.
    /// Present on instance overrides to identify which occurrence is modified.
    /// The shared uid links this override back to its master event.
    pub recurrence_id: Option<EventTime>,

    // Alarms & Availability
    pub reminders: Reminders,
    /// Whether event blocks time (OPAQUE) or is free (TRANSPARENT)
    pub transparency: Transparency,

    // Meeting Data
    pub organizer: Option<Attendee>,
    pub attendees: Vec<Attendee>,
    pub conference_url: Option<String>,

    // Sync Infrastructure (excluded from PartialEq)
    /// Last modification timestamp (LAST-MODIFIED)
    pub updated: Option<DateTime<Utc>>,
    /// Revision sequence number (SEQUENCE)
    pub sequence: Option<i64>,

    // Provider-specific (excluded from PartialEq)
    /// Non-standard X-properties carried through sync round-trips
    /// (e.g. `X-GOOGLE-EVENT-ID`, `X-ALT-DESC`).
    pub custom_properties: Vec<CustomProperty>,
}

/// A non-standard X-property, e.g:
/// `X-GOOGLE-EVENT-ID`
/// X-ALT-DESC` with `FMTTYPE=x/html` param
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomProperty {
    pub name: String,
    pub value: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub params: Vec<(String, String)>,
}

impl CustomProperty {
    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            value: value.into(),
            params: Vec::new(),
        }
    }

    pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.params.push((key.into(), value.into()));
        self
    }
}

impl PartialEq for Event {
    fn eq(&self, other: &Self) -> bool {
        self.uid == other.uid
            && self.summary == other.summary
            && self.description == other.description
            && self.location == other.location
            && self.start == other.start
            && self.end == other.end
            && self.status == other.status
            && self.recurrence == other.recurrence
            && self.recurrence_id == other.recurrence_id
            && self.reminders == other.reminders
            && self.transparency == other.transparency
            && self.organizer == other.organizer
            && self.attendees == other.attendees
            && self.conference_url == other.conference_url
    }
}

impl fmt::Display for Event {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.summary.is_empty() {
            write!(f, "(Unknown Event)")
        } else {
            write!(f, "{}", self.summary)
        }
    }
}

impl Event {
    fn generate_uid() -> String {
        format!("{}@caldir", uuid::Uuid::new_v4())
    }

    pub fn new(
        summary: String,
        start: EventTime,
        end: EventTime,
        description: Option<String>,
        location: Option<String>,
        recurrence: Option<Recurrence>,
        reminders: Vec<Reminder>,
    ) -> Self {
        let uid = Self::generate_uid();

        Event {
            uid,
            summary,
            description,
            location,
            start,
            end,
            status: EventStatus::Confirmed,
            recurrence,
            recurrence_id: None,
            reminders: Reminders(reminders),
            transparency: Transparency::Opaque,
            organizer: None,
            attendees: Vec::new(),
            conference_url: None,
            updated: None,
            sequence: None,
            custom_properties: Vec::new(),
        }
    }

    /// Return a clone of this event with a fresh UID.
    pub fn with_new_uid(&self) -> Self {
        Event {
            uid: Self::generate_uid(),
            ..self.clone()
        }
    }

    /// Returns the unique identifier for this event based on RFC 5545 identity.
    /// Format: `{uid}` for non-recurring events, `{uid}__{recurrence_id}` for instances.
    pub fn unique_id(&self) -> String {
        match &self.recurrence_id {
            Some(rid) => format!("{}__{}", self.uid, rid.to_ics_string()),
            None => self.uid.clone(),
        }
    }

    /// Find the attendee matching the given email (case-insensitive)
    pub fn find_attendee(&self, email: &str) -> Option<&Attendee> {
        self.attendees
            .iter()
            .find(|a| a.email.eq_ignore_ascii_case(email))
    }

    /// True if email is an attendee but NOT the organizer
    pub fn is_invite_for(&self, email: &str) -> bool {
        let is_attendee = self.find_attendee(email).is_some();
        let is_organizer = self
            .organizer
            .as_ref()
            .is_some_and(|o| o.email.eq_ignore_ascii_case(email));
        is_attendee && !is_organizer
    }

    /// Get the user's participation status for this event
    pub fn my_status(&self, email: &str) -> Option<ParticipationStatus> {
        self.find_attendee(email)?.response_status
    }

    /// True if this is a pending invite (NEEDS-ACTION) for the given email
    pub fn is_pending_invite_for(&self, email: &str) -> bool {
        self.is_invite_for(email) && self.my_status(email) == Some(ParticipationStatus::NeedsAction)
    }

    /// Return a new Event with the attendee's PARTSTAT updated, sequence bumped, and updated timestamp set
    pub fn with_response(&self, email: &str, status: ParticipationStatus) -> Option<Event> {
        // Verify the email is an attendee
        self.find_attendee(email)?;

        let mut updated = self.clone();
        for attendee in &mut updated.attendees {
            if attendee.email.eq_ignore_ascii_case(email) {
                attendee.response_status = Some(status);
            }
        }
        updated.sequence = Some(self.sequence.unwrap_or(0) + 1);
        updated.updated = Some(Utc::now());
        Some(updated)
    }

    /// Look up a custom (X-) property's value by name.
    pub fn custom_property(&self, name: &str) -> Option<&str> {
        self.custom_properties
            .iter()
            .find(|p| p.name == name)
            .map(|p| p.value.as_str())
    }

    /// Whether this event's start falls inside `[from, to]`, interpreting
    /// floating/all-day starts in `host_tz`. Cancelled events never match.
    pub fn starts_in_range<Tz: TimeZone>(
        &self,
        from: DateTime<Utc>,
        to: DateTime<Utc>,
        host_tz: &Tz,
    ) -> bool {
        if self.status == EventStatus::Cancelled {
            return false;
        }
        let Some(instant) = self.start.resolve_instant_in_zone(host_tz) else {
            return false;
        };
        instant >= from && instant <= to
    }
}

/// An event attendee (also used for organizer)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Attendee {
    pub name: Option<String>,
    pub email: String,
    /// Participation status (RFC 5545 PARTSTAT)
    pub response_status: Option<ParticipationStatus>,
}

/// Participation status for an attendee (RFC 5545 PARTSTAT)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum ParticipationStatus {
    Accepted,
    Declined,
    Tentative,
    NeedsAction,
}

impl ParticipationStatus {
    /// Convert to ICS PARTSTAT string (RFC 5545)
    pub fn as_ics_str(&self) -> &'static str {
        match self {
            Self::Accepted => "ACCEPTED",
            Self::Declined => "DECLINED",
            Self::Tentative => "TENTATIVE",
            Self::NeedsAction => "NEEDS-ACTION",
        }
    }

    /// Parse from ICS PARTSTAT string
    pub fn from_ics_str(s: &str) -> Option<Self> {
        match s {
            "ACCEPTED" => Some(Self::Accepted),
            "DECLINED" => Some(Self::Declined),
            "TENTATIVE" => Some(Self::Tentative),
            "NEEDS-ACTION" => Some(Self::NeedsAction),
            _ => None,
        }
    }
}

impl fmt::Display for ParticipationStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::Accepted => "accepted",
            Self::Declined => "declined",
            Self::Tentative => "maybe",
            Self::NeedsAction => "pending",
        };
        write!(f, "{}", s)
    }
}

impl FromStr for ParticipationStatus {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "accepted" | "accept" | "yes" | "y" => Ok(Self::Accepted),
            "declined" | "decline" | "no" | "n" => Ok(Self::Declined),
            "tentative" | "maybe" | "m" => Ok(Self::Tentative),
            "needs-action" | "needs_action" | "pending" => Ok(Self::NeedsAction),
            _ => Err(format!("Unknown participation status: '{}'", s)),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Reminder {
    /// Minutes before the event to trigger
    pub minutes: i64,
}

impl Reminder {
    /// Parse a human-readable duration string like "10m", "1h", "2 days" into a Reminder.
    pub fn from_duration_str(input: &str) -> Result<Self, String> {
        let dur = humantime::parse_duration(input)
            .map_err(|_| format!("Could not parse reminder: \"{}\"", input))?;
        let minutes = (dur.as_secs() / 60) as i64;
        Ok(Reminder { minutes })
    }
}

/// A collection of reminders with order-independent equality.
///
/// Reminder order is not meaningful — `[10, 30]` and `[30, 10]` are equal.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reminders(pub Vec<Reminder>);

impl PartialEq for Reminders {
    fn eq(&self, other: &Self) -> bool {
        let mut a = self.0.clone();
        let mut b = other.0.clone();
        a.sort_by_key(|r| r.minutes);
        b.sort_by_key(|r| r.minutes);
        a == b
    }
}

impl std::ops::Deref for Reminders {
    type Target = Vec<Reminder>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl std::ops::DerefMut for Reminders {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl FromIterator<Reminder> for Reminders {
    fn from_iter<I: IntoIterator<Item = Reminder>>(iter: I) -> Self {
        Reminders(iter.into_iter().collect())
    }
}

impl<'a> IntoIterator for &'a Reminders {
    type Item = &'a Reminder;
    type IntoIter = std::slice::Iter<'a, Reminder>;
    fn into_iter(self) -> Self::IntoIter {
        self.0.iter()
    }
}

/// Event transparency (busy/free status)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Transparency {
    /// Event blocks time on calendar (default)
    Opaque,
    /// Event does not block time (shows as free)
    Transparent,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EventStatus {
    Confirmed,
    Tentative,
    Cancelled,
}

#[cfg(test)]
mod tests {
    use crate::event_time::EventTime;

    use super::*;
    use chrono::NaiveDate;

    fn make_event_with_attendees() -> Event {
        let start = EventTime::Date(NaiveDate::from_ymd_opt(2026, 3, 15).unwrap());
        let end = EventTime::Date(NaiveDate::from_ymd_opt(2026, 3, 15).unwrap());
        let mut event = Event::new("Team standup".into(), start, end, None, None, None, vec![]);
        event.organizer = Some(Attendee {
            name: Some("Alice".into()),
            email: "alice@example.com".into(),
            response_status: Some(ParticipationStatus::Accepted),
        });
        event.attendees = vec![
            Attendee {
                name: Some("Alice".into()),
                email: "alice@example.com".into(),
                response_status: Some(ParticipationStatus::Accepted),
            },
            Attendee {
                name: Some("Bob".into()),
                email: "bob@example.com".into(),
                response_status: Some(ParticipationStatus::NeedsAction),
            },
            Attendee {
                name: Some("Carol".into()),
                email: "carol@example.com".into(),
                response_status: Some(ParticipationStatus::Tentative),
            },
        ];
        event
    }

    #[test]
    fn find_attendee_case_insensitive() {
        let event = make_event_with_attendees();
        assert!(event.find_attendee("bob@example.com").is_some());
        assert!(event.find_attendee("BOB@EXAMPLE.COM").is_some());
        assert!(event.find_attendee("unknown@example.com").is_none());
    }

    #[test]
    fn is_invite_for_attendee_not_organizer() {
        let event = make_event_with_attendees();
        // Bob is attendee but not organizer
        assert!(event.is_invite_for("bob@example.com"));
        // Alice is both attendee and organizer → not an invite
        assert!(!event.is_invite_for("alice@example.com"));
        // Unknown person
        assert!(!event.is_invite_for("unknown@example.com"));
    }

    #[test]
    fn my_status_returns_correct_status() {
        let event = make_event_with_attendees();
        assert_eq!(
            event.my_status("bob@example.com"),
            Some(ParticipationStatus::NeedsAction)
        );
        assert_eq!(
            event.my_status("carol@example.com"),
            Some(ParticipationStatus::Tentative)
        );
        assert_eq!(event.my_status("unknown@example.com"), None);
    }

    #[test]
    fn is_pending_invite_for() {
        let event = make_event_with_attendees();
        // Bob has NEEDS-ACTION
        assert!(event.is_pending_invite_for("bob@example.com"));
        // Carol has TENTATIVE, not pending
        assert!(!event.is_pending_invite_for("carol@example.com"));
        // Alice is organizer
        assert!(!event.is_pending_invite_for("alice@example.com"));
    }

    #[test]
    fn with_response_updates_status() {
        let event = make_event_with_attendees();
        let updated = event
            .with_response("bob@example.com", ParticipationStatus::Accepted)
            .unwrap();

        assert_eq!(
            updated
                .find_attendee("bob@example.com")
                .unwrap()
                .response_status,
            Some(ParticipationStatus::Accepted)
        );
        // Sequence bumped
        assert_eq!(updated.sequence, Some(1));
        // Updated timestamp set
        assert!(updated.updated.is_some());
    }

    #[test]
    fn with_response_returns_none_for_unknown() {
        let event = make_event_with_attendees();
        assert!(
            event
                .with_response("unknown@example.com", ParticipationStatus::Accepted)
                .is_none()
        );
    }

    #[test]
    fn with_response_bumps_existing_sequence() {
        let mut event = make_event_with_attendees();
        event.sequence = Some(5);
        let updated = event
            .with_response("bob@example.com", ParticipationStatus::Declined)
            .unwrap();
        assert_eq!(updated.sequence, Some(6));
    }

    #[test]
    fn participation_status_display() {
        assert_eq!(ParticipationStatus::Accepted.to_string(), "accepted");
        assert_eq!(ParticipationStatus::Declined.to_string(), "declined");
        assert_eq!(ParticipationStatus::Tentative.to_string(), "maybe");
        assert_eq!(ParticipationStatus::NeedsAction.to_string(), "pending");
    }

    /// Floating "9am" on a Pacific host fires at 17:00 UTC, not 09:00 UTC.
    #[test]
    fn starts_in_range_resolves_floating_in_host_zone() {
        use chrono::FixedOffset;
        let pacific = FixedOffset::west_opt(8 * 3600).unwrap();
        let naive = NaiveDate::from_ymd_opt(2026, 1, 15)
            .unwrap()
            .and_hms_opt(9, 0, 0)
            .unwrap();
        let event = Event::new(
            "Test".into(),
            EventTime::DateTimeFloating(naive),
            EventTime::DateTimeFloating(naive),
            None,
            None,
            None,
            vec![],
        );
        let from = Utc.with_ymd_and_hms(2026, 1, 15, 16, 0, 0).unwrap();
        let to = Utc.with_ymd_and_hms(2026, 1, 15, 18, 0, 0).unwrap();
        assert!(event.starts_in_range(from, to, &pacific));
    }

    #[test]
    fn participation_status_from_str() {
        // Primary names
        assert_eq!("accepted".parse(), Ok(ParticipationStatus::Accepted));
        assert_eq!("declined".parse(), Ok(ParticipationStatus::Declined));
        assert_eq!("tentative".parse(), Ok(ParticipationStatus::Tentative));

        // Aliases
        assert_eq!("accept".parse(), Ok(ParticipationStatus::Accepted));
        assert_eq!("yes".parse(), Ok(ParticipationStatus::Accepted));
        assert_eq!("y".parse(), Ok(ParticipationStatus::Accepted));
        assert_eq!("decline".parse(), Ok(ParticipationStatus::Declined));
        assert_eq!("no".parse(), Ok(ParticipationStatus::Declined));
        assert_eq!("n".parse(), Ok(ParticipationStatus::Declined));
        assert_eq!("maybe".parse(), Ok(ParticipationStatus::Tentative));
        assert_eq!("m".parse(), Ok(ParticipationStatus::Tentative));

        // Case insensitive
        assert_eq!("ACCEPT".parse(), Ok(ParticipationStatus::Accepted));
        assert_eq!("Yes".parse(), Ok(ParticipationStatus::Accepted));

        // Invalid
        assert!("invalid".parse::<ParticipationStatus>().is_err());
    }
}
