//! LQP authentication via League Client lockfile.
//!
//! The League Client writes a lockfile on startup containing connection credentials.

use std::fs;
use std::path::PathBuf;
use std::time::Duration;

use tracing::{debug, trace, warn};

use crate::error::{LqpError, Result};

/// League Client lockfile location (relative to League Client install).
pub const LOCKFILE_NAME: &str = "lockfile";

/// Default League Client paths on Windows.
pub const WINDOWS_CLIENT_PATHS: &[&str] = &[
    "C:\\Riot Games\\League of Legends",
    "D:\\Riot Games\\League of Legends",
];

/// Credentials extracted from the League Client lockfile.
#[derive(Debug, Clone)]
pub struct LockfileCredentials {
    /// Process name (usually "LeagueClient").
    pub process_name: String,
    /// Process ID.
    pub pid: u32,
    /// Port for the LQP API.
    pub port: u16,
    /// Password for authentication.
    pub password: String,
    /// Protocol (usually "https").
    pub protocol: String,
}

impl LockfileCredentials {
    /// Parse lockfile contents into credentials.
    ///
    /// Lockfile format: `LeagueClient:PID:PORT:PASSWORD:PROTOCOL`
    pub fn parse(contents: &str) -> Result<Self> {
        let parts: Vec<&str> = contents.trim().split(':').collect();

        if parts.len() != 5 {
            return Err(LqpError::LockfileParseError(format!(
                "Expected 5 fields, got {}",
                parts.len()
            ))
            .into());
        }

        let process_name = parts[0].to_string();
        let pid = parts[1]
            .parse::<u32>()
            .map_err(|e| LqpError::LockfileParseError(format!("Invalid PID: {}", e)))?;
        let port = parts[2]
            .parse::<u16>()
            .map_err(|e| LqpError::LockfileParseError(format!("Invalid port: {}", e)))?;
        let password = parts[3].to_string();
        let protocol = parts[4].to_string();

        Ok(Self {
            process_name,
            pid,
            port,
            password,
            protocol,
        })
    }

    /// Get the base URL for the LQP REST API.
    pub fn base_url(&self) -> String {
        format!("{}://127.0.0.1:{}", self.protocol, self.port)
    }

    /// Get the WebSocket URL for the LQP WebSocket API.
    pub fn ws_url(&self) -> String {
        format!("wss://127.0.0.1:{}", self.port)
    }

    /// Get the Basic Auth header value.
    pub fn auth_header(&self) -> String {
        let credentials = format!("riot:{}", self.password);
        let encoded =
            base64::Engine::encode(&base64::engine::general_purpose::STANDARD, credentials);
        format!("Basic {}", encoded)
    }
}

/// Watcher for League Client lockfile.
///
/// Monitors for the League Client starting and stopping by watching
/// for the lockfile to appear/disappear.
pub struct LockfileWatcher {
    /// Possible lockfile paths to check.
    paths: Vec<PathBuf>,
    /// Current credentials if client is running.
    current: Option<LockfileCredentials>,
}

impl LockfileWatcher {
    /// Create a new lockfile watcher with default paths.
    pub fn new() -> Self {
        let paths = Self::discover_lockfile_paths();
        Self {
            paths,
            current: None,
        }
    }

    /// Create a watcher with a specific lockfile path.
    pub fn with_path(path: PathBuf) -> Self {
        Self {
            paths: vec![path],
            current: None,
        }
    }

    /// Discover possible lockfile paths based on OS.
    fn discover_lockfile_paths() -> Vec<PathBuf> {
        // For now, League of Legends is not available on Linux.
        // There is no point in trying to locate the client.
        if cfg!(target_os = "windows") {
            WINDOWS_CLIENT_PATHS
                .iter()
                .map(|s| PathBuf::from(s).join(LOCKFILE_NAME))
                .collect()
        } else {
            Vec::new()
        }
    }

    /// Check if the League Client is currently running.
    pub fn is_client_running(&self) -> bool {
        self.current.is_some()
    }

    /// Get current credentials if available.
    pub fn credentials(&self) -> Option<&LockfileCredentials> {
        self.current.as_ref()
    }

    /// Check for lockfile and update credentials.
    ///
    /// Returns:
    /// - `Some(true)` if client just started
    /// - `Some(false)` if client just stopped
    /// - `None` if no change
    pub fn check(&mut self) -> Result<Option<bool>> {
        // Try to find and read lockfile
        let found = self.find_lockfile()?;

        match (found, &self.current) {
            (Some(creds), None) => {
                // Client started
                debug!(
                    "League Client detected (PID: {}, Port: {})",
                    creds.pid, creds.port
                );
                self.current = Some(creds);
                Ok(Some(true))
            }
            (Some(new_creds), Some(old_creds)) => {
                // Client still running, check if restarted
                if new_creds.pid != old_creds.pid {
                    debug!(
                        "League Client restarted (PID: {} -> {})",
                        old_creds.pid, new_creds.pid
                    );
                    self.current = Some(new_creds);
                    // Don't report as change, just update
                    Ok(None)
                } else {
                    // No change
                    Ok(None)
                }
            }
            (None, Some(_)) => {
                // Client stopped
                debug!("League Client stopped");
                self.current = None;
                Ok(Some(false))
            }
            (None, None) => {
                // Still not running
                Ok(None)
            }
        }
    }

    /// Find and read the lockfile.
    fn find_lockfile(&self) -> Result<Option<LockfileCredentials>> {
        for path in &self.paths {
            trace!("Checking lockfile path: {:?}", path);

            if path.exists() {
                match fs::read_to_string(path) {
                    Ok(contents) => match LockfileCredentials::parse(&contents) {
                        Ok(creds) => return Ok(Some(creds)),
                        Err(e) => {
                            warn!("Failed to parse lockfile at {:?}: {}", path, e);
                            continue;
                        }
                    },
                    Err(e) => {
                        trace!("Failed to read lockfile at {:?}: {}", path, e);
                        continue;
                    }
                }
            }
        }

        Ok(None)
    }

    /// Wait for the League Client to start.
    ///
    /// Blocks until the client is detected or timeout is reached.
    pub async fn wait_for_client(&mut self, timeout: Duration) -> Result<LockfileCredentials> {
        let start = std::time::Instant::now();

        loop {
            if let Some(creds) = self.find_lockfile()? {
                self.current = Some(creds.clone());
                return Ok(creds);
            }

            if start.elapsed() >= timeout {
                return Err(LqpError::Timeout.into());
            }

            tokio::time::sleep(Duration::from_millis(500)).await;
        }
    }
}

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

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

    #[test]
    fn test_parse_lockfile() {
        let contents = "LeagueClient:12345:52436:abc123:https";
        let creds = LockfileCredentials::parse(contents).unwrap();

        assert_eq!(creds.process_name, "LeagueClient");
        assert_eq!(creds.pid, 12345);
        assert_eq!(creds.port, 52436);
        assert_eq!(creds.password, "abc123");
        assert_eq!(creds.protocol, "https");
    }

    #[test]
    fn test_base_url() {
        let creds = LockfileCredentials {
            process_name: "LeagueClient".to_string(),
            pid: 12345,
            port: 52436,
            password: "abc123".to_string(),
            protocol: "https".to_string(),
        };

        assert_eq!(creds.base_url(), "https://127.0.0.1:52436");
    }

    #[test]
    fn test_auth_header() {
        let creds = LockfileCredentials {
            process_name: "LeagueClient".to_string(),
            pid: 12345,
            port: 52436,
            password: "abc123".to_string(),
            protocol: "https".to_string(),
        };

        // base64("riot:abc123") = "cmlvdDphYmMxMjM="
        assert!(creds.auth_header().contains("Basic "));
    }
}
