//! Build Coordinator
//!
//! Centralized coordination system to prevent duplicate builds and ensure safe
//! artifact generation. This module implements mutex/lockfile mechanisms and
//! artifact freshness checking.

use std::{
    collections::HashMap,
    fs::{self, File, OpenOptions},
    io::{self, Read, Write},
    path::{Path, PathBuf},
    sync::{Arc, Mutex},
    time::{Duration, SystemTime, UNIX_EPOCH},
};

use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum CoordinatorError {
    #[error("IO error: {0}")]
    Io(#[from] io::Error),
    #[error("Serialization error: {0}")]
    Json(#[from] serde_json::Error),
    #[error("Build lock timeout: {0}")]
    LockTimeout(String),
    #[error("Artifact validation failed: {0}")]
    ValidationFailed(String),
}

type Result<T> = std::result::Result<T, CoordinatorError>;

/// Build artifact metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactInfo {
    pub name: String,
    pub output_path: PathBuf,
    pub source_paths: Vec<PathBuf>,
    pub last_built: u64,
    pub checksum: String,
    pub build_time_ms: u64,
}

/// Build coordination state
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct BuildState {
    pub artifacts: HashMap<String, ArtifactInfo>,
    pub last_cleanup: u64,
}

/// Main build coordinator
pub struct BuildCoordinator {
    state_file: PathBuf,
    lock_dir: PathBuf,
    state: Arc<Mutex<BuildState>>,
    timeout: Duration,
}

impl BuildCoordinator {
    /// Create a new build coordinator
    pub fn new<P: AsRef<Path>>(workspace_root: P) -> Result<Self> {
        let workspace_root = workspace_root.as_ref();
        let state_file = workspace_root.join("target").join("build-state.json");
        let lock_dir = workspace_root.join("target").join("build-locks");

        // Ensure directories exist
        if let Some(parent) = state_file.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::create_dir_all(&lock_dir)?;

        let state = Arc::new(Mutex::new(Self::load_state(&state_file)?));

        Ok(Self {
            state_file,
            lock_dir,
            state,
            timeout: Duration::from_secs(30),
        })
    }

    /// Load build state from file
    fn load_state(state_file: &Path) -> Result<BuildState> {
        if state_file.exists() {
            let mut file = File::open(state_file)?;
            let mut contents = String::new();
            file.read_to_string(&mut contents)?;
            Ok(serde_json::from_str(&contents)?)
        } else {
            Ok(BuildState::default())
        }
    }

    /// Save build state to file
    pub fn save_state(&self) -> Result<()> {
        let state = self.state.lock().unwrap();
        let contents = serde_json::to_string_pretty(&*state)?;

        // Atomic write: write to temp file then rename
        let temp_file = self.state_file.with_extension("json.tmp");
        let mut file = File::create(&temp_file)?;
        file.write_all(contents.as_bytes())?;
        file.sync_all()?;
        drop(file);

        fs::rename(temp_file, &self.state_file)?;
        Ok(())
    }

    /// Check if artifact needs rebuilding
    pub fn needs_rebuild(&self, artifact_name: &str, source_paths: &[PathBuf]) -> Result<bool> {
        let state = self.state.lock().unwrap();

        let artifact = match state.artifacts.get(artifact_name) {
            Some(artifact) => artifact,
            None => return Ok(true), // Not built yet
        };

        // Check if output exists
        if !artifact.output_path.exists() {
            return Ok(true);
        }

        // Check if any source is newer than last build
        let last_built = UNIX_EPOCH + Duration::from_secs(artifact.last_built);

        for source_path in source_paths {
            if source_path.exists() {
                let source_modified = fs::metadata(source_path)?.modified()?;
                if source_modified > last_built {
                    return Ok(true);
                }
            }
        }

        // Check if source paths changed
        if artifact.source_paths != source_paths {
            return Ok(true);
        }

        Ok(false)
    }

    /// Acquire build lock with timeout
    pub fn acquire_lock(&self, artifact_name: &str) -> Result<BuildLock> {
        let lock_file = self.lock_dir.join(format!("{}.lock", artifact_name));
        let start_time = std::time::Instant::now();

        loop {
            match OpenOptions::new()
                .write(true)
                .create_new(true)
                .open(&lock_file)
            {
                Ok(mut file) => {
                    // Write PID to lock file
                    write!(file, "{}", std::process::id())?;
                    file.sync_all()?;

                    return Ok(BuildLock {
                        lock_file,
                        _file: file,
                    });
                }
                Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
                    // Check if lock is stale
                    if let Ok(mut existing_file) = File::open(&lock_file) {
                        let mut pid_str = String::new();
                        if existing_file.read_to_string(&mut pid_str).is_ok() {
                            if let Ok(pid) = pid_str.trim().parse::<u32>() {
                                // Check if process is still running (Unix-specific)
                                #[cfg(unix)]
                                {
                                    use std::process::Command;
                                    let output =
                                        Command::new("ps").arg("-p").arg(pid.to_string()).output();

                                    if let Ok(output) = output {
                                        if !output.status.success() {
                                            // Process doesn't exist, remove stale lock
                                            let _ = fs::remove_file(&lock_file);
                                            continue;
                                        }
                                    }
                                }
                            }
                        }
                    }

                    if start_time.elapsed() > self.timeout {
                        return Err(CoordinatorError::LockTimeout(format!(
                            "Failed to acquire lock for {} after {:?}",
                            artifact_name, self.timeout
                        )));
                    }

                    std::thread::sleep(Duration::from_millis(100));
                }
                Err(e) => return Err(CoordinatorError::Io(e)),
            }
        }
    }

    /// Update artifact info after successful build
    pub fn update_artifact(
        &self,
        name: String,
        output_path: PathBuf,
        source_paths: Vec<PathBuf>,
        build_time_ms: u64,
    ) -> Result<()> {
        let checksum = self.calculate_checksum(&output_path)?;
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();

        let artifact_info = ArtifactInfo {
            name: name.clone(),
            output_path,
            source_paths,
            last_built: now,
            checksum,
            build_time_ms,
        };

        {
            let mut state = self.state.lock().unwrap();
            state.artifacts.insert(name, artifact_info);
        }

        self.save_state()
    }

    /// Calculate file checksum for validation
    fn calculate_checksum(&self, path: &Path) -> Result<String> {
        if !path.exists() {
            return Ok("missing".to_string());
        }

        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        let metadata = fs::metadata(path)?;
        let mut hasher = DefaultHasher::new();

        // Hash file size and modified time
        metadata.len().hash(&mut hasher);
        if let Ok(modified) = metadata.modified() {
            if let Ok(duration) = modified.duration_since(UNIX_EPOCH) {
                duration.as_secs().hash(&mut hasher);
            }
        }

        Ok(format!("{:x}", hasher.finish()))
    }

    /// Clean up old artifacts and locks
    pub fn cleanup(&self) -> Result<()> {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();

        {
            let mut state = self.state.lock().unwrap();

            // Only cleanup once per day
            if now - state.last_cleanup < 86400 {
                return Ok(());
            }

            // Remove artifacts whose outputs no longer exist
            state
                .artifacts
                .retain(|_, artifact| artifact.output_path.exists());

            state.last_cleanup = now;
        }

        // Clean up stale lock files
        if let Ok(entries) = fs::read_dir(&self.lock_dir) {
            for entry in entries.flatten() {
                if let Ok(metadata) = entry.metadata() {
                    if let Ok(created) = metadata.created() {
                        if created.elapsed().unwrap_or_default() > Duration::from_secs(3600) {
                            // Remove locks older than 1 hour
                            let _ = fs::remove_file(entry.path());
                        }
                    }
                }
            }
        }

        self.save_state()
    }

    /// Get build statistics
    pub fn get_stats(&self) -> HashMap<String, serde_json::Value> {
        let state = self.state.lock().unwrap();
        let mut stats = HashMap::new();

        let total_artifacts = state.artifacts.len();
        let total_build_time: u64 = state.artifacts.values().map(|a| a.build_time_ms).sum();

        stats.insert("total_artifacts".to_string(), total_artifacts.into());
        stats.insert("total_build_time_ms".to_string(), total_build_time.into());

        if total_artifacts > 0 {
            let avg_build_time = total_build_time / total_artifacts as u64;
            stats.insert("avg_build_time_ms".to_string(), avg_build_time.into());
        }

        stats
    }
}

/// RAII lock guard that automatically releases lock on drop
pub struct BuildLock {
    lock_file: PathBuf,
    _file: File,
}

impl Drop for BuildLock {
    fn drop(&mut self) {
        let _ = fs::remove_file(&self.lock_file);
    }
}

/// Global coordinator instance
static COORDINATOR: std::sync::OnceLock<Arc<Mutex<BuildCoordinator>>> = std::sync::OnceLock::new();

/// Get or initialize global coordinator
pub fn get_coordinator() -> Result<Arc<Mutex<BuildCoordinator>>> {
    // Use get_or_init with a fallback approach for error handling
    let coordinator_result = COORDINATOR.get_or_init(|| {
        let workspace_root = std::env::var("CARGO_WORKSPACE_DIR")
            .or_else(|_| std::env::var("CARGO_MANIFEST_DIR"))
            .unwrap_or_else(|_| ".".to_string()); // Fallback to current directory

        match BuildCoordinator::new(workspace_root) {
            Ok(coordinator) => Arc::new(Mutex::new(coordinator)),
            Err(_) => {
                // Create a minimal coordinator if initialization fails
                let workspace_root = PathBuf::from(".");
                let state_file = workspace_root.join("target").join("build-state.json");
                let lock_dir = workspace_root.join("target").join("build-locks");

                // Create directories if they don't exist
                std::fs::create_dir_all(&lock_dir).ok();
                if let Some(parent) = state_file.parent() {
                    std::fs::create_dir_all(parent).ok();
                }

                let fallback_coordinator = BuildCoordinator {
                    state_file,
                    lock_dir,
                    state: Arc::new(Mutex::new(BuildState::default())),
                    timeout: Duration::from_secs(30),
                };
                Arc::new(Mutex::new(fallback_coordinator))
            }
        }
    });

    Ok(coordinator_result.clone())
}

/// High-level function to coordinate artifact build
pub fn coordinate_build<F>(
    artifact_name: &str,
    output_path: PathBuf,
    source_paths: Vec<PathBuf>,
    builder: F,
) -> Result<bool>
where
    F: FnOnce() -> Result<()>,
{
    let coordinator = get_coordinator()?;
    let coordinator = coordinator.lock().unwrap();

    // Check if rebuild is needed
    if !coordinator.needs_rebuild(artifact_name, &source_paths)? {
        println!(
            "cargo:warning=Skipping {}: artifact is up-to-date",
            artifact_name
        );
        return Ok(false);
    }

    // Acquire build lock
    let _lock = coordinator.acquire_lock(artifact_name)?;

    // Double-check after acquiring lock (another build might have completed)
    if !coordinator.needs_rebuild(artifact_name, &source_paths)? {
        println!(
            "cargo:warning=Skipping {}: artifact was built by another process",
            artifact_name
        );
        return Ok(false);
    }

    // Run the build
    let start_time = std::time::Instant::now();
    builder()?;
    let build_time_ms = start_time.elapsed().as_millis() as u64;

    // Validate output was created
    if !output_path.exists() {
        return Err(CoordinatorError::ValidationFailed(format!(
            "Expected output {} was not created",
            output_path.display()
        )));
    }

    // Update artifact info
    coordinator.update_artifact(
        artifact_name.to_string(),
        output_path,
        source_paths,
        build_time_ms,
    )?;

    println!(
        "cargo:warning=Successfully built {} in {}ms",
        artifact_name, build_time_ms
    );
    Ok(true)
}
