//! Timeline storage backend.

use std::collections::HashMap;
use std::path::PathBuf;

use chrono::{DateTime, Duration, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::error::{Result, TimelineError};
use crate::recording::RecordingResult;

/// A timestamped event in the timeline.
/// Stores raw API data only — no typed event parsing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimestampedEvent {
    /// Video timestamp (offset from recording start).
    pub video_timestamp: Duration,
    /// Game timestamp (in-game time).
    pub game_timestamp: Option<Duration>,
    /// Real-world timestamp.
    pub timestamp: DateTime<Utc>,
    /// Event type string derived from URI (e.g. "game_start", "lp_change", "kill").
    pub event_type: String,
    /// Human-readable description for logging.
    #[serde(default)]
    pub description: String,
    /// The raw JSON data from the API — the single source of truth.
    /// The tauri-app parses the values it needs from this data.
    #[serde(default)]
    pub raw_data: serde_json::Value,
    /// URI of the endpoint that triggered this event.
    #[serde(default)]
    pub uri: String,
}

/// Metadata for a recording.
/// Stores raw API responses for maximum flexibility and future-proofing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordingMetadata {
    /// Unique recording ID.
    pub id: Uuid,
    /// Game ID if available.
    pub game_id: Option<u64>,
    /// Raw session data from `/lol-gameflow/v1/session`.
    #[serde(default)]
    pub raw_session: Option<serde_json::Value>,
    /// Raw summoner data from `/lol-summoner/v1/current-summoner`.
    #[serde(default)]
    pub raw_summoner: Option<serde_json::Value>,
    /// Raw champion select data from `/lol-champ-select/v1/session`.
    #[serde(default)]
    pub raw_champion_select: Option<serde_json::Value>,
    /// Raw rune page data from `/lol-perks/v1/currentpage`.
    #[serde(default)]
    pub raw_rune_page: Option<serde_json::Value>,
    /// Raw live client data from `/liveclientdata/allgamedata`.
    #[serde(default)]
    pub raw_live_client_data: Option<serde_json::Value>,
    /// Raw end-of-game stats from `/lol-end-of-game/v1/eog-stats-block`.
    #[serde(default)]
    pub raw_end_game_stats: Option<serde_json::Value>,
    /// Recording start time.
    pub start_time: DateTime<Utc>,
    /// Recording end time.
    pub end_time: Option<DateTime<Utc>>,
    /// Recording duration.
    pub duration: Duration,
    /// Output file path.
    pub file_path: Option<PathBuf>,
    /// Video file name (just the filename, not the full path).
    pub video_file: Option<String>,
    /// File size in bytes.
    pub file_size: Option<u64>,
    /// Number of events.
    pub event_count: usize,
    /// Whether the timeline has been finalized.
    pub finalized: bool,
}

/// Update for recording metadata.
#[derive(Debug, Clone, Default)]
pub struct MetadataUpdate {
    pub game_id: Option<u64>,
    pub raw_session: Option<serde_json::Value>,
    pub raw_summoner: Option<serde_json::Value>,
    pub raw_champion_select: Option<serde_json::Value>,
    pub raw_rune_page: Option<serde_json::Value>,
    pub raw_live_client_data: Option<serde_json::Value>,
    pub raw_end_game_stats: Option<serde_json::Value>,
}

impl RecordingMetadata {
    /// Create metadata from a recording result.
    pub fn from_result(result: &RecordingResult) -> Self {
        Self {
            id: Uuid::new_v4(),
            game_id: result.game_id,
            raw_session: None,
            raw_summoner: None,
            raw_champion_select: None,
            raw_rune_page: None,
            raw_live_client_data: None,
            raw_end_game_stats: None,
            start_time: result.start_time,
            end_time: Some(result.end_time),
            duration: result.duration,
            file_path: Some(result.path.clone()),
            video_file: result
                .path
                .file_name()
                .and_then(|n| n.to_str())
                .map(String::from),
            file_size: result.file_size(),
            event_count: 0,
            finalized: false,
        }
    }
}

/// In-memory timeline storage.
pub struct TimelineStore {
    /// Recording metadata by ID.
    recordings: RwLock<HashMap<Uuid, RecordingMetadata>>,
    /// Timelines by recording ID.
    timelines: RwLock<HashMap<Uuid, Vec<TimestampedEvent>>>,
    /// Storage directory for persistence.
    storage_dir: PathBuf,
}

impl TimelineStore {
    /// Create a new timeline store.
    pub fn new() -> Self {
        let storage_dir = crate::config::get_default_output_dir()
            .unwrap_or_else(|| PathBuf::from("./recordings"))
            .join("timelines");

        Self {
            recordings: RwLock::new(HashMap::new()),
            timelines: RwLock::new(HashMap::new()),
            storage_dir,
        }
    }

    /// Create a timeline store with a specific storage directory.
    pub fn with_dir(storage_dir: PathBuf) -> Self {
        Self {
            recordings: RwLock::new(HashMap::new()),
            timelines: RwLock::new(HashMap::new()),
            storage_dir,
        }
    }

    /// Start a new recording entry (called when recording begins).
    /// Returns the recording ID for tracking events during recording.
    pub fn start_recording_entry(&self, game_id: Option<u64>, _champion: Option<String>) -> Uuid {
        let id = Uuid::new_v4();
        let metadata = RecordingMetadata {
            id,
            game_id,
            raw_session: None,
            raw_summoner: None,
            raw_champion_select: None,
            raw_rune_page: None,
            raw_live_client_data: None,
            raw_end_game_stats: None,
            start_time: Utc::now(),
            end_time: None,
            duration: Duration::zero(),
            file_path: None,
            video_file: None,
            file_size: None,
            event_count: 0,
            finalized: false,
        };

        self.recordings.write().insert(id, metadata);
        self.timelines.write().insert(id, Vec::new());

        id
    }

    /// Finalize a recording with the recording result.
    /// Called when recording stops.
    pub fn finalize_recording(&self, recording_id: Uuid, result: RecordingResult) -> Result<()> {
        let mut recordings = self.recordings.write();

        let metadata = recordings
            .get_mut(&recording_id)
            .ok_or(TimelineError::RecordingNotFound(recording_id))?;

        // Update with final recording data
        metadata.start_time = result.start_time;
        metadata.end_time = Some(result.end_time);
        metadata.duration = result.duration;
        metadata.file_path = Some(result.path.clone());
        metadata.video_file = result
            .path
            .file_name()
            .and_then(|n| n.to_str())
            .map(String::from);
        metadata.file_size = result.file_size();
        metadata.finalized = true;

        // Persist to disk
        drop(recordings);
        self.persist_recording(recording_id)?;

        Ok(())
    }

    /// Add a new recording (legacy method for backwards compatibility).
    pub fn add_recording(&self, result: RecordingResult) -> Result<Uuid> {
        let id = Uuid::new_v4();
        let metadata = RecordingMetadata {
            id,
            game_id: result.game_id,
            raw_session: None,
            raw_summoner: None,
            raw_champion_select: None,
            raw_rune_page: None,
            raw_live_client_data: None,
            raw_end_game_stats: None,
            start_time: result.start_time,
            end_time: Some(result.end_time),
            duration: result.duration,
            file_path: Some(result.path.clone()),
            video_file: result
                .path
                .file_name()
                .and_then(|n| n.to_str())
                .map(String::from),
            file_size: result.file_size(),
            event_count: 0,
            finalized: true,
        };

        self.recordings.write().insert(id, metadata);
        self.timelines.write().insert(id, Vec::new());

        // Persist to disk
        self.persist_recording(id)?;

        Ok(id)
    }

    /// Add an event to a recording's timeline.
    pub fn add_event(&self, recording_id: Uuid, event: TimestampedEvent) -> Result<()> {
        let mut timelines = self.timelines.write();

        let timeline = timelines
            .get_mut(&recording_id)
            .ok_or(TimelineError::RecordingNotFound(recording_id))?;

        timeline.push(event);

        // Update event count in metadata
        drop(timelines);
        let mut recordings = self.recordings.write();
        if let Some(metadata) = recordings.get_mut(&recording_id) {
            metadata.event_count += 1;
        }

        Ok(())
    }

    /// Update metadata for a recording.
    pub fn update_metadata(&self, recording_id: Uuid, update: MetadataUpdate) -> Result<()> {
        let mut recordings = self.recordings.write();
        if let Some(metadata) = recordings.get_mut(&recording_id) {
            if let Some(game_id) = update.game_id {
                metadata.game_id = Some(game_id);
            }
            if let Some(raw_session) = update.raw_session {
                metadata.raw_session = Some(raw_session);
            }
            if let Some(raw_summoner) = update.raw_summoner {
                metadata.raw_summoner = Some(raw_summoner);
            }
            if let Some(raw_champion_select) = update.raw_champion_select {
                metadata.raw_champion_select = Some(raw_champion_select);
            }
            if let Some(raw_rune_page) = update.raw_rune_page {
                metadata.raw_rune_page = Some(raw_rune_page);
            }
            if let Some(raw_live_client_data) = update.raw_live_client_data {
                metadata.raw_live_client_data = Some(raw_live_client_data);
            }
            if let Some(raw_end_game_stats) = update.raw_end_game_stats {
                metadata.raw_end_game_stats = Some(raw_end_game_stats);
            }
        }
        drop(recordings);
        self.persist_recording(recording_id)?;
        Ok(())
    }

    /// Get all recordings.
    pub fn get_all_recordings(&self) -> Result<Vec<RecordingMetadata>> {
        let recordings = self.recordings.read();
        Ok(recordings.values().cloned().collect())
    }

    /// Get a specific recording.
    pub fn get_recording(&self, id: Uuid) -> Result<RecordingMetadata> {
        self.recordings
            .read()
            .get(&id)
            .cloned()
            .ok_or_else(|| TimelineError::RecordingNotFound(id).into())
    }

    /// Get the timeline for a recording.
    pub fn get_timeline(&self, recording_id: Uuid) -> Result<super::Timeline> {
        let recordings = self.recordings.read();
        let metadata = recordings
            .get(&recording_id)
            .ok_or(TimelineError::RecordingNotFound(recording_id))?;

        let timelines = self.timelines.read();
        let events = timelines.get(&recording_id).cloned().unwrap_or_default();

        Ok(super::Timeline {
            recording_id,
            start_time: metadata.start_time,
            end_time: metadata.end_time,
            duration_secs: metadata.duration.num_seconds(),
            events,
            game_id: metadata.game_id,
            raw_session: metadata.raw_session.clone(),
            raw_summoner: metadata.raw_summoner.clone(),
            raw_champion_select: metadata.raw_champion_select.clone(),
            raw_rune_page: metadata.raw_rune_page.clone(),
            raw_live_client_data: metadata.raw_live_client_data.clone(),
            raw_end_game_stats: metadata.raw_end_game_stats.clone(),
            video_file: metadata.video_file.clone(),
        })
    }

    /// Delete a recording and its timeline.
    pub fn delete_recording(&self, id: Uuid) -> Result<()> {
        // Remove from memory
        self.recordings.write().remove(&id);
        self.timelines.write().remove(&id);

        // Remove from disk
        let timeline_file = self.storage_dir.join(format!("{}.json", id));
        if timeline_file.exists() {
            std::fs::remove_file(&timeline_file)?;
        }

        Ok(())
    }

    /// Persist a recording to disk.
    fn persist_recording(&self, id: Uuid) -> Result<()> {
        // Ensure storage directory exists
        if !self.storage_dir.exists() {
            std::fs::create_dir_all(&self.storage_dir)?;
        }

        let metadata = self.recordings.read().get(&id).cloned();

        let events = self.timelines.read().get(&id).cloned();

        if let (Some(metadata), Some(events)) = (metadata, events) {
            let timeline = super::Timeline {
                recording_id: id,
                start_time: metadata.start_time,
                end_time: metadata.end_time,
                duration_secs: metadata.duration.num_seconds(),
                events,
                game_id: metadata.game_id,
                raw_session: metadata.raw_session,
                raw_summoner: metadata.raw_summoner,
                raw_champion_select: metadata.raw_champion_select,
                raw_rune_page: metadata.raw_rune_page,
                raw_live_client_data: metadata.raw_live_client_data,
                raw_end_game_stats: metadata.raw_end_game_stats,
                video_file: metadata.video_file,
            };

            let file_path = self.storage_dir.join(format!("{}.json", id));
            let json = serde_json::to_string_pretty(&timeline)?;
            std::fs::write(&file_path, json)?;
        }

        Ok(())
    }

    /// Load all recordings from disk.
    pub fn load_from_disk(&self) -> Result<()> {
        if !self.storage_dir.exists() {
            return Ok(());
        }

        for entry in std::fs::read_dir(&self.storage_dir)? {
            let entry = entry?;
            let path = entry.path();

            if path.extension().map(|e| e == "json").unwrap_or(false) {
                if let Ok(contents) = std::fs::read_to_string(&path) {
                    if let Ok(timeline) = serde_json::from_str::<super::Timeline>(&contents) {
                        let recording_id = timeline.recording_id;
                        let start_time = timeline.start_time;
                        let end_time = timeline.end_time;
                        let duration = timeline.duration();
                        let event_count = timeline.events.len();
                        let game_id = timeline.game_id;
                        let raw_session = timeline.raw_session;
                        let raw_summoner = timeline.raw_summoner;
                        let raw_champion_select = timeline.raw_champion_select;
                        let raw_rune_page = timeline.raw_rune_page;
                        let raw_live_client_data = timeline.raw_live_client_data;
                        let raw_end_game_stats = timeline.raw_end_game_stats;
                        let video_file = timeline.video_file;
                        let events = timeline.events;

                        let metadata = RecordingMetadata {
                            id: recording_id,
                            game_id,
                            raw_session,
                            raw_summoner,
                            raw_champion_select,
                            raw_rune_page,
                            raw_live_client_data,
                            raw_end_game_stats,
                            start_time,
                            end_time,
                            duration,
                            file_path: None,
                            video_file,
                            file_size: None,
                            event_count,
                            finalized: true,
                        };

                        self.recordings.write().insert(recording_id, metadata);
                        self.timelines.write().insert(recording_id, events);
                    }
                }
            }
        }

        Ok(())
    }
}

impl Default for TimelineStore {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_timeline_store_creation() {
        let store = TimelineStore::new();
        let recordings = store.get_all_recordings().unwrap();
        assert!(recordings.is_empty());
    }

    #[test]
    fn test_add_recording() {
        let store = TimelineStore::new();

        let result = RecordingResult {
            path: PathBuf::from("/tmp/test.mp4"),
            game_id: Some(12345),
            champion: Some("Ahri".to_string()),
            start_time: Utc::now(),
            end_time: Utc::now() + Duration::seconds(60),
            duration: Duration::seconds(60),
            stats: None,
        };

        let id = store.add_recording(result).unwrap();
        let recordings = store.get_all_recordings().unwrap();
        assert_eq!(recordings.len(), 1);

        let metadata = store.get_recording(id).unwrap();
        assert_eq!(metadata.game_id, Some(12345));
    }
}
