use anyhow::{Context, Result};
use uuid::Uuid;

/// Map upload, storage, and rotation management.
///
/// Handles custom map files (.map) for Rust servers. Maps are stored
/// in object storage with metadata in the database. Supports rotation
/// strategies where different maps are used on successive wipes.
pub struct MapManager {
    db: sqlx::PgPool,
    storage_base_path: String,
}

impl MapManager {
    pub fn new(db: sqlx::PgPool, storage_base_path: String) -> Self {
        Self {
            db,
            storage_base_path,
        }
    }

    /// Upload a new map file to storage and register it in the database.
    pub async fn upload_map(
        &self,
        license_id: Uuid,
        display_name: &str,
        filename: &str,
        data: &[u8],
    ) -> Result<Uuid> {
        // Compute checksum of map data
        use sha2::{Digest, Sha256};
        let mut hasher = Sha256::new();
        hasher.update(data);
        let checksum = hex::encode(hasher.finalize());

        // Generate UUID for this map
        let map_id = Uuid::new_v4();

        // Write file to storage
        let storage_path = format!("{}/{}/{}.map", self.storage_base_path, license_id, map_id);

        // Create parent directory if it doesn't exist
        if let Some(parent) = std::path::Path::new(&storage_path).parent() {
            tokio::fs::create_dir_all(parent)
                .await
                .context("Failed to create map storage directory")?;
        }

        tokio::fs::write(&storage_path, data)
            .await
            .context("Failed to write map file to storage")?;

        // Insert map_entry record in DB
        sqlx::query(
            "INSERT INTO map_library (id, license_id, display_name, file_name, storage_path, file_size_bytes, checksum, created_at)
             VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())",
        )
        .bind(map_id)
        .bind(license_id)
        .bind(display_name)
        .bind(filename)
        .bind(&storage_path)
        .bind(data.len() as i64)
        .bind(&checksum)
        .execute(&self.db)
        .await
        .context("Failed to insert map record")?;

        tracing::info!(
            "Map uploaded: {} ({}) for license {}",
            display_name,
            map_id,
            license_id
        );

        Ok(map_id)
    }

    /// Delete a map from storage and the database.
    pub async fn delete_map(&self, map_id: Uuid) -> Result<()> {
        // Load map_entry from DB
        let map: Option<(String,)> = sqlx::query_as(
            "SELECT storage_path FROM map_library WHERE id = $1",
        )
        .bind(map_id)
        .fetch_optional(&self.db)
        .await
        .context("Failed to query map for deletion")?;

        if let Some((storage_path,)) = map {
            // Delete file from storage
            if tokio::fs::remove_file(&storage_path).await.is_err() {
                tracing::warn!("Failed to delete map file from storage: {}", storage_path);
            }

            // Delete DB record
            sqlx::query("DELETE FROM map_library WHERE id = $1")
                .bind(map_id)
                .execute(&self.db)
                .await
                .context("Failed to delete map record")?;

            // Clear any wipe profile references to this map
            sqlx::query("UPDATE wipe_profiles SET map_id = NULL WHERE map_id = $1")
                .bind(map_id)
                .execute(&self.db)
                .await
                .context("Failed to clear wipe profile references")?;

            tracing::info!("Map deleted: {}", map_id);
        }

        Ok(())
    }

    /// Get the current map rotation for a license (ordered list of map IDs).
    pub async fn get_rotation(&self, license_id: Uuid) -> Result<Vec<Uuid>> {
        let rotation: Vec<(Uuid,)> = sqlx::query_as(
            "SELECT m.id
             FROM map_rotations mr
             JOIN map_library m ON m.id = mr.map_id
             WHERE mr.license_id = $1
             ORDER BY mr.rotation_order ASC",
        )
        .bind(license_id)
        .fetch_all(&self.db)
        .await
        .context("Failed to query map rotation")?;

        Ok(rotation.into_iter().map(|(id,)| id).collect())
    }

    /// Advance the rotation to the next map in sequence.
    /// Returns the map ID that should be used for the next wipe.
    pub async fn advance_rotation(&self, license_id: Uuid) -> Result<Uuid> {
        // Get current rotation position
        let current_position: Option<(i32,)> = sqlx::query_as(
            "SELECT current_position FROM map_rotation_state WHERE license_id = $1",
        )
        .bind(license_id)
        .fetch_optional(&self.db)
        .await
        .context("Failed to query rotation state")?;

        let current_pos = current_position.map(|(p,)| p).unwrap_or(0);

        // Get rotation list
        let rotation = self.get_rotation(license_id).await?;

        if rotation.is_empty() {
            anyhow::bail!("No maps in rotation for license {}", license_id);
        }

        // Advance to next position (wrap around)
        let next_pos = (current_pos + 1) % (rotation.len() as i32);
        let next_map_id = rotation[next_pos as usize];

        // Update rotation position in DB
        sqlx::query(
            "INSERT INTO map_rotation_state (license_id, current_position)
             VALUES ($1, $2)
             ON CONFLICT (license_id)
             DO UPDATE SET current_position = $2",
        )
        .bind(license_id)
        .bind(next_pos)
        .execute(&self.db)
        .await
        .context("Failed to update rotation position")?;

        tracing::debug!(
            "Map rotation advanced for license {}: position {} → map {}",
            license_id,
            next_pos,
            next_map_id
        );

        Ok(next_map_id)
    }

    /// Generate a time-limited signed URL for downloading a map file.
    /// Used by the companion agent or panel adapter to fetch map files.
    pub async fn generate_signed_url(&self, map_id: Uuid) -> Result<String> {
        // Load map_entry from DB
        let map: Option<(String,)> = sqlx::query_as(
            "SELECT storage_path FROM map_library WHERE id = $1",
        )
        .bind(map_id)
        .fetch_optional(&self.db)
        .await
        .context("Failed to query map for signed URL")?;

        if let Some((storage_path,)) = map {
            // For local file storage, we generate a simple token-based URL
            // In production, this would use S3 pre-signed URLs
            let token = crate::services::encryption::generate_token(32);

            // Store the token with expiration in a temporary auth table
            sqlx::query(
                "INSERT INTO temporary_download_tokens (token, resource_path, expires_at)
                 VALUES ($1, $2, NOW() + INTERVAL '15 minutes')",
            )
            .bind(&token)
            .bind(&storage_path)
            .execute(&self.db)
            .await
            .context("Failed to create download token")?;

            // Return URL with token
            Ok(format!("/api/maps/download/{}/{}", map_id, token))
        } else {
            anyhow::bail!("Map not found: {}", map_id);
        }
    }

    /// Retrieve map data by ID (for internal use).
    pub async fn get_map_data(&self, map_id: Uuid) -> Result<Vec<u8>> {
        // Load map storage path
        let map: Option<(String,)> = sqlx::query_as(
            "SELECT storage_path FROM map_library WHERE id = $1",
        )
        .bind(map_id)
        .fetch_optional(&self.db)
        .await
        .context("Failed to query map")?;

        if let Some((storage_path,)) = map {
            let data = tokio::fs::read(&storage_path)
                .await
                .with_context(|| format!("Failed to read map file: {}", storage_path))?;

            Ok(data)
        } else {
            anyhow::bail!("Map not found: {}", map_id);
        }
    }
}
