use anyhow::{Context, Result};
use log::warn;
use std::fs::{self, OpenOptions};
use std::path::{Path, PathBuf};

use super::types::{FrameCounts, HistoryCounts, HistoryDepth, SessionInspection};
use crate::session::lock::{lock_shared, unlock};
use crate::session::options::SessionOptions;
use crate::session::snapshot;
use crate::session::snapshot::BoardPagesSnapshot;

/// Inspect the current session file for CLI reporting.
pub fn inspect_session(options: &SessionOptions) -> Result<SessionInspection> {
    let prefix = options.file_prefix();
    let mut session_path = options.session_file_path();
    let mut session_identity = options.output_identity().map(|s| s.to_string());
    let mut metadata = fs::metadata(&session_path).ok();

    if metadata.is_none()
        && options.per_output
        && options.output_identity().is_none()
        && let Some((path, identity)) = find_existing_variant(&options.base_dir, &prefix, ".json")
    {
        metadata = fs::metadata(&path).ok();
        session_path = path;
        session_identity = identity;
    }

    let exists = metadata.is_some();
    let size_bytes = metadata.as_ref().map(|m| m.len());
    let modified = metadata.as_ref().and_then(|m| m.modified().ok());

    let mut backup_path = options.backup_file_path();
    let mut backup_meta = fs::metadata(&backup_path).ok();
    if backup_meta.is_none()
        && options.per_output
        && options.output_identity().is_none()
        && let Some((path, _)) = find_existing_variant(&options.base_dir, &prefix, ".json.bak")
    {
        backup_meta = fs::metadata(&path).ok();
        backup_path = path;
    }

    let backup_exists = backup_meta.is_some();
    let backup_size = backup_meta.as_ref().map(|m| m.len());

    let mut frame_counts = None;
    let mut tool_state_present = false;
    let mut compressed = false;
    let mut history_counts = None;
    let mut history_present = false;
    let mut file_version = None;

    if exists {
        let lock_path = session_path.with_extension("lock");
        let lock_file = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .truncate(true)
            .open(&lock_path)
            .with_context(|| format!("failed to open session lock file {}", lock_path.display()))?;
        lock_shared(&lock_file)
            .with_context(|| format!("failed to acquire shared lock {}", lock_path.display()))?;

        let loaded = snapshot::load_snapshot_inner(&session_path, options);

        if let Err(err) = unlock(&lock_file) {
            warn!(
                "failed to unlock session file {}: {}",
                lock_path.display(),
                err
            );
        }

        if let Some(loaded) = loaded? {
            let snapshot = loaded.snapshot;
            frame_counts = Some(FrameCounts {
                transparent: page_shape_count(board_pages(&snapshot, "transparent")),
                whiteboard: page_shape_count(board_pages(&snapshot, "whiteboard")),
                blackboard: page_shape_count(board_pages(&snapshot, "blackboard")),
            });
            let counts = HistoryCounts {
                transparent: history_depth_from_pages(board_pages(&snapshot, "transparent")),
                whiteboard: history_depth_from_pages(board_pages(&snapshot, "whiteboard")),
                blackboard: history_depth_from_pages(board_pages(&snapshot, "blackboard")),
            };
            history_present = counts.has_history();
            history_counts = Some(counts);
            tool_state_present = snapshot.tool_state.is_some();
            compressed = loaded.compressed;
            file_version = Some(loaded.version);
        }
    }

    Ok(SessionInspection {
        session_path,
        exists,
        size_bytes,
        modified,
        backup_path,
        backup_exists,
        backup_size_bytes: backup_size,
        active_identity: session_identity,
        per_output: options.per_output,
        persist_transparent: options.persist_transparent,
        persist_whiteboard: options.persist_whiteboard,
        persist_blackboard: options.persist_blackboard,
        persist_history: options.persist_history,
        restore_tool_state: options.restore_tool_state,
        history_limit: options.max_persisted_undo_depth,
        frame_counts,
        history_counts,
        history_present,
        tool_state_present,
        compressed,
        file_version,
    })
}

fn board_pages<'a>(
    snapshot: &'a crate::session::snapshot::SessionSnapshot,
    id: &str,
) -> Option<&'a BoardPagesSnapshot> {
    snapshot
        .boards
        .iter()
        .find(|board| board.id == id)
        .map(|board| &board.pages)
}

fn history_depth_from_pages(pages: Option<&BoardPagesSnapshot>) -> HistoryDepth {
    if let Some(pages) = pages {
        HistoryDepth {
            undo: pages.pages.iter().map(|page| page.undo_stack_len()).sum(),
            redo: pages.pages.iter().map(|page| page.redo_stack_len()).sum(),
        }
    } else {
        HistoryDepth::default()
    }
}

fn page_shape_count(pages: Option<&BoardPagesSnapshot>) -> usize {
    pages
        .map(|pages| pages.pages.iter().map(|page| page.shapes.len()).sum())
        .unwrap_or(0)
}

fn find_existing_variant(
    dir: &Path,
    prefix: &str,
    suffix: &str,
) -> Option<(PathBuf, Option<String>)> {
    let entries = fs::read_dir(dir).ok()?;
    let mut matches: Vec<(PathBuf, Option<String>)> = Vec::new();

    for entry in entries {
        let entry = entry.ok()?;
        let path = entry.path();
        if !path.is_file() {
            continue;
        }
        if let Some(name) = path
            .file_name()
            .and_then(|n| n.to_str())
            .map(|s| s.to_string())
            && name.starts_with(prefix)
            && name.ends_with(suffix)
        {
            matches.push((path, extract_identity(&name, prefix, suffix)));
        }
    }

    matches.sort_by(|a, b| {
        let a_name = a.0.file_name().and_then(|n| n.to_str()).unwrap_or_default();
        let b_name = b.0.file_name().and_then(|n| n.to_str()).unwrap_or_default();
        a_name.cmp(b_name)
    });

    matches.into_iter().next()
}

fn extract_identity(name: &str, prefix: &str, suffix: &str) -> Option<String> {
    if !name.starts_with(prefix) || !name.ends_with(suffix) {
        return None;
    }

    let start = prefix.len();
    let end = name.len() - suffix.len();
    if start >= end {
        return None;
    }

    let trimmed = name[start..end].trim_start_matches('-');
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed.to_string())
    }
}

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

    #[test]
    fn extract_identity_parses_suffix_and_prefix() {
        let identity =
            extract_identity("session-display_1-DP_1.json", "session-display_1", ".json");
        assert_eq!(identity.as_deref(), Some("DP_1"));
    }

    #[test]
    fn extract_identity_rejects_missing_suffix_or_identity() {
        assert!(extract_identity("session-display_1.json", "session-display_1", ".json").is_none());
        assert!(extract_identity("other-display.json", "session-display_1", ".json").is_none());
    }
}
