use chrono::{DateTime, Local, NaiveDateTime};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LogEvent {
    NpcDialogue {
        at: DateTime<Local>,
        speaker: String,
        text: String,
    },
    ZoneTransition {
        at: DateTime<Local>,
        zone: String,
    },
}

impl LogEvent {
    pub fn label(&self) -> &'static str {
        match self {
            Self::NpcDialogue { .. } => "NPC",
            Self::ZoneTransition { .. } => "ZONE",
        }
    }

    pub fn text(&self) -> &str {
        match self {
            Self::NpcDialogue { text, .. } => text,
            Self::ZoneTransition { zone, .. } => zone,
        }
    }

    pub fn occurred_at(&self) -> DateTime<Local> {
        match self {
            Self::NpcDialogue { at, .. } | Self::ZoneTransition { at, .. } => *at,
        }
    }
}

pub fn parse_line(line: &str) -> Option<LogEvent> {
    let (timestamp, message) = split_timestamp(line)?;

    if let Some((speaker, text)) = parse_npc_says(message) {
        return Some(LogEvent::NpcDialogue {
            at: timestamp,
            speaker,
            text,
        });
    }

    message
        .strip_prefix("You have entered ")
        .and_then(|zone| zone.strip_suffix('.'))
        .filter(|zone| !zone.is_empty())
        .map(|zone| LogEvent::ZoneTransition {
            at: timestamp,
            zone: zone.to_string(),
        })
}

fn split_timestamp(line: &str) -> Option<(DateTime<Local>, &str)> {
    let end = line.find("] ")?;
    let raw_timestamp = line.strip_prefix('[')?.get(..end - 1)?;
    let timestamp = NaiveDateTime::parse_from_str(raw_timestamp, "%a %b %d %H:%M:%S %Y")
        .ok()?
        .and_local_timezone(Local)
        .single()?;
    Some((timestamp, &line[end + 2..]))
}

fn parse_npc_says(message: &str) -> Option<(String, String)> {
    let (speaker, quote) = message.split_once(" says, '")?;
    let text = quote.strip_suffix('\'')?;
    if speaker.is_empty() || text.is_empty() || speaker == "You" {
        return None;
    }
    Some((speaker.to_string(), text.to_string()))
}

pub fn speaker_key(name: &str) -> String {
    let normalized = name
        .chars()
        .flat_map(char::to_lowercase)
        .map(|character| {
            if character.is_ascii_alphanumeric() {
                character
            } else {
                '_'
            }
        })
        .collect::<String>();
    let normalized = normalized.trim_matches('_');
    let compact = normalized
        .split('_')
        .filter(|part| !part.is_empty())
        .collect::<Vec<_>>()
        .join("_");
    format!("npc:{compact}")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_npc_dialogue_with_apostrophe() {
        let event =
            parse_line("[Fri Jul 17 20:10:29 2026] Turga says, 'I've got a Rusty Long Sword.'")
                .unwrap();
        assert_eq!(
            event,
            LogEvent::NpcDialogue {
                at: NaiveDateTime::parse_from_str(
                    "Fri Jul 17 20:10:29 2026",
                    "%a %b %d %H:%M:%S %Y"
                )
                .unwrap()
                .and_local_timezone(Local)
                .single()
                .unwrap(),
                speaker: "Turga".into(),
                text: "I've got a Rusty Long Sword.".into(),
            }
        );
    }

    #[test]
    fn ignores_player_dialogue_and_channel_tells() {
        assert!(parse_line("[Fri Jul 17 20:10:29 2026] You say, 'Hail'").is_none());
        assert!(parse_line("[Fri Jul 17 20:10:29 2026] Rath tells General:2, 'hello'").is_none());
    }

    #[test]
    fn parses_zone_message() {
        let event =
            parse_line("[Fri Jul 17 20:09:32 2026] You have entered Lake Rathetear.").unwrap();
        assert!(matches!(event, LogEvent::ZoneTransition { zone, .. } if zone == "Lake Rathetear"));
    }

    #[test]
    fn normalizes_speaker_keys() {
        assert_eq!(speaker_key("A lizardman healer"), "npc:a_lizardman_healer");
        assert_eq!(speaker_key("Zok  Caropni!"), "npc:zok_caropni");
    }
}
