use std::path::{Path, PathBuf};
use std::fs;
use chrono::{DateTime, Utc};
use sqlx::PgPool;
use uuid::Uuid;
use walkdir::WalkDir;
use crate::error::{ApiError, Result};
use crate::models::media::MediaItem;
use crate::utils::media_parsing::parse_media_title;

pub struct MediaScanner {
    pool: PgPool,
}

#[derive(Debug, Clone)]
pub struct ScannedFile {
    pub path: PathBuf,
    pub size: u64,
    pub is_video: bool,
    pub nfo_path: Option<PathBuf>,
}

impl MediaScanner {
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }

    /// Scan a directory for media files and update the database
    pub async fn scan_directory(&self, scan_path: &str) -> Result<Vec<MediaItem>> {
        tracing::info!("Starting media scan of directory: {}", scan_path);
        
        let path = Path::new(scan_path);
        if !path.exists() {
            return Err(ApiError::NotFound(format!("Directory does not exist: {}", scan_path)));
        }

        // Find all media files
        let scanned_files = self.find_media_files(path)?;
        tracing::info!("Found {} potential media files", scanned_files.len());

        let mut media_items = Vec::new();
        let mut errors = Vec::new();

        let total_files = scanned_files.len();

        // Process each file
        for scanned_file in &scanned_files {
            match self.process_media_file(scanned_file.clone()).await {
                Ok(Some(media_item)) => media_items.push(media_item),
                Ok(None) => {
                    tracing::debug!("Skipped file (already exists or no changes)");
                }
                Err(e) => {
                    tracing::error!("Error processing file {:?}: {:?}", scanned_file.path, e);
                    errors.push(format!("{:?}: {:?}", scanned_file.path, e));
                }
            }
        }

        // Update scan status
        self.update_scan_status(scan_path, media_items.len() as i32, total_files as i32, errors).await?;

        tracing::info!("Media scan completed. Processed {} files, {} successful", scanned_files.len(), media_items.len());
        Ok(media_items)
    }

    /// Find all media files in a directory
    fn find_media_files(&self, path: &Path) -> Result<Vec<ScannedFile>> {
        let mut files = Vec::new();
        
        for entry in WalkDir::new(path)
            .follow_links(false)
            .into_iter()
            .filter_map(|e| e.ok())
        {
            let path = entry.path();
            
            // Skip directories
            if !path.is_file() {
                continue;
            }

            // Check if it's a media file
            if let Some(extension) = path.extension() {
                let ext = extension.to_string_lossy().to_lowercase();
                let is_media = matches!(ext.as_str(), 
                    "mp4" | "mkv" | "avi" | "mov" | "webm" | 
                    "mp3" | "m4a" | "flac" | "wav" | "ogg"
                );

                if is_media {
                    let metadata = fs::metadata(path).map_err(|e| {
                        ApiError::Internal(format!("Failed to get file metadata: {}", e))
                    })?;

                    let is_video = matches!(ext.as_str(), "mp4" | "mkv" | "avi" | "mov" | "webm");
                    
                    // Look for corresponding NFO file
                    let nfo_path = path.with_extension("nfo");
                    let nfo_exists = nfo_path.exists().then_some(nfo_path);

                    files.push(ScannedFile {
                        path: path.to_path_buf(),
                        size: metadata.len(),
                        is_video,
                        nfo_path: nfo_exists,
                    });
                }
            }
        }

        Ok(files)
    }

    /// Process a single media file
    async fn process_media_file(&self, scanned_file: ScannedFile) -> Result<Option<MediaItem>> {
        let file_path_str = scanned_file.path.to_string_lossy().to_string();
        
        // Check if file already exists in database
        if let Some(existing) = self.get_existing_media_item(&file_path_str).await? {
            // Check if file has been modified since last scan
            let file_metadata = fs::metadata(&scanned_file.path).map_err(|e| {
                ApiError::Internal(format!("Failed to get file metadata: {}", e))
            })?;
            
            let file_modified = file_metadata.modified().map_err(|e| {
                ApiError::Internal(format!("Failed to get file modification time: {}", e))
            })?;
            
            let file_modified_utc = DateTime::<Utc>::from(file_modified);
            
            // If file hasn't been modified since last scan, skip it
            if let Some(last_scanned) = existing.last_scanned {
                if file_modified_utc <= last_scanned {
                    return Ok(None);
                }
            }
        }

        // Extract media information
        let media_info = self.extract_media_info(&scanned_file).await?;
        
        // Parse NFO file if it exists
        let nfo_metadata = if let Some(ref nfo_path) = scanned_file.nfo_path {
            self.parse_nfo_file(nfo_path).await.unwrap_or_default()
        } else {
            NFOMetadata::default()
        };

        // Parse filename for title, speaker, and date if NFO doesn't provide complete info
        let filename_parsed = self.extract_parsed_info_from_filename(&scanned_file.path);
        
        // Create or update media item
        let media_item = MediaItem {
            id: Uuid::new_v4(), // Will be ignored if updating
            title: nfo_metadata.title.unwrap_or(filename_parsed.title),
            speaker: nfo_metadata.speaker.or(filename_parsed.speaker),
            date: nfo_metadata.date.or(filename_parsed.date),
            description: nfo_metadata.description,
            scripture_reading: nfo_metadata.scripture_reading,
            file_path: file_path_str.clone(),
            file_size: Some(scanned_file.size as i64),
            duration_seconds: media_info.duration_seconds,
            video_codec: media_info.video_codec,
            audio_codec: media_info.audio_codec,
            resolution: media_info.resolution,
            bitrate: media_info.bitrate,
            thumbnail_path: None, // Will be generated later
            thumbnail_generated_at: None,
            nfo_path: scanned_file.nfo_path.map(|p| p.to_string_lossy().to_string()),
            last_scanned: Some(Utc::now()),
            created_at: Some(Utc::now()), // Will be ignored if updating
            updated_at: Some(Utc::now()),
        };

        // Insert or update in database
        let mut saved_item = self.save_media_item(media_item).await?;
        
        // Generate thumbnail for video files
        if scanned_file.is_video {
            if let Ok(thumbnail_path) = self.generate_thumbnail(&scanned_file.path, &saved_item.id).await {
                // Update the database with thumbnail path
                saved_item = self.update_thumbnail_path(saved_item.id, &thumbnail_path).await?;
            }
        }
        
        tracing::info!("Processed media file: {} -> {}", file_path_str, saved_item.title);
        Ok(Some(saved_item))
    }

    /// Extract media information using GStreamer discoverer
    async fn extract_media_info(&self, scanned_file: &ScannedFile) -> Result<MediaInfo> {
        let file_path = scanned_file.path.to_string_lossy();
        
        use gstreamer::prelude::*;
        use gstreamer_pbutils::prelude::*;
        
        // Initialize GStreamer if not already done
        let _ = gstreamer::init();
        
        // Create discoverer
        let timeout = gstreamer::ClockTime::from_seconds(10);
        let discoverer = gstreamer_pbutils::Discoverer::new(timeout)
            .map_err(|e| ApiError::Internal(format!("Failed to create discoverer: {}", e)))?;
        
        // Create file URI
        let file_uri = format!("file://{}", scanned_file.path.to_string_lossy());
        
        // Discover media info
        let info = match discoverer.discover_uri(&file_uri) {
            Ok(info) => info,
            Err(e) => {
                tracing::warn!("GStreamer discoverer failed for {}: {}", file_path, e);
                // Fallback to basic detection
                return Ok(MediaInfo {
                    duration_seconds: None,
                    video_codec: if scanned_file.is_video { 
                        Some(self.guess_codec_from_extension(&scanned_file.path))
                    } else { 
                        None 
                    },
                    audio_codec: Some("aac".to_string()),
                    resolution: if scanned_file.is_video { Some("1920x1080".to_string()) } else { None },
                    bitrate: None,
                });
            }
        };
        
        let mut media_info = MediaInfo {
            duration_seconds: None,
            video_codec: None,
            audio_codec: None,
            resolution: None,
            bitrate: None,
        };
        
        // Extract duration
        let duration_ns = info.duration();
        if let Some(duration) = duration_ns {
            media_info.duration_seconds = Some((duration.seconds()) as i32);
        }
        
        // Extract stream information
        let stream_list = info.stream_list();
        
        for stream_info in stream_list {
            if let Some(video_info) = stream_info.downcast_ref::<gstreamer_pbutils::DiscovererVideoInfo>() {
                // Video stream info
                if media_info.video_codec.is_none() {
                    if let Some(caps) = video_info.caps() {
                        let structure = caps.structure(0);
                        if let Some(structure) = structure {
                            let codec_name = structure.name();
                            media_info.video_codec = Some(Self::map_gst_codec_to_name(codec_name));
                        }
                    }
                }
                
                // Extract resolution
                if media_info.resolution.is_none() {
                    let width = video_info.width();
                    let height = video_info.height();
                    if width > 0 && height > 0 {
                        media_info.resolution = Some(format!("{}x{}", width, height));
                    }
                }
                
                // Extract bitrate if available
                if media_info.bitrate.is_none() {
                    let bitrate = video_info.bitrate();
                    if bitrate > 0 {
                        media_info.bitrate = Some(bitrate as i32);
                    }
                }
            } else if let Some(audio_info) = stream_info.downcast_ref::<gstreamer_pbutils::DiscovererAudioInfo>() {
                // Audio stream info
                if media_info.audio_codec.is_none() {
                    if let Some(caps) = audio_info.caps() {
                        let structure = caps.structure(0);
                        if let Some(structure) = structure {
                            let codec_name = structure.name();
                            media_info.audio_codec = Some(Self::map_gst_codec_to_name(codec_name));
                        }
                    }
                }
            }
        }
        
        tracing::debug!("Discovered media info for {}: duration={}s, video={:?}, audio={:?}, resolution={:?}", 
            file_path, 
            media_info.duration_seconds.unwrap_or(0),
            media_info.video_codec,
            media_info.audio_codec, 
            media_info.resolution
        );
        
        Ok(media_info)
    }
    
    /// Map GStreamer codec names to human-readable format
    fn map_gst_codec_to_name(gst_name: &str) -> String {
        match gst_name {
            "video/x-h264" => "h264".to_string(),
            "video/x-h265" => "hevc".to_string(),
            "video/x-av1" => "av1".to_string(),
            "video/x-vp8" => "vp8".to_string(),
            "video/x-vp9" => "vp9".to_string(),
            "audio/mpeg" => "mp3".to_string(),
            "audio/x-aac" => "aac".to_string(),
            "audio/x-flac" => "flac".to_string(),
            "audio/x-vorbis" => "vorbis".to_string(),
            "audio/x-opus" => "opus".to_string(),
            _ => {
                // Extract the format part after the last slash
                if let Some(last_slash) = gst_name.rfind('/') {
                    let format = &gst_name[last_slash + 1..];
                    // Remove common prefixes
                    if format.starts_with("x-") {
                        format[2..].to_string()
                    } else {
                        format.to_string()
                    }
                } else {
                    gst_name.to_string()
                }
            }
        }
    }

    fn guess_codec_from_extension(&self, path: &Path) -> String {
        if let Some(extension) = path.extension() {
            match extension.to_string_lossy().to_lowercase().as_str() {
                "mp4" => "h264".to_string(), // Most MP4s are H.264, we'll detect AV1 with FFprobe later
                "webm" => "vp9".to_string(),
                "mkv" => "h264".to_string(), // Default assumption
                _ => "unknown".to_string(),
            }
        } else {
            "unknown".to_string()
        }
    }

    fn extract_parsed_info_from_filename(&self, path: &Path) -> crate::utils::media_parsing::ParsedMediaTitle {
        let filename = path.file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("Unknown");
        
        parse_media_title(filename)
    }

    async fn get_existing_media_item(&self, file_path: &str) -> Result<Option<MediaItem>> {
        let item = sqlx::query_as!(
            MediaItem,
            r#"
            SELECT id, title, speaker, date, description, scripture_reading,
                   file_path, file_size, duration_seconds, video_codec, audio_codec,
                   resolution, bitrate, thumbnail_path, thumbnail_generated_at,
                   nfo_path, last_scanned, created_at, updated_at
            FROM media_items 
            WHERE file_path = $1
            "#,
            file_path
        )
        .fetch_optional(&self.pool)
        .await
        .map_err(|e| ApiError::Database(e.to_string()))?;

        Ok(item)
    }

    async fn save_media_item(&self, media_item: MediaItem) -> Result<MediaItem> {
        let saved = sqlx::query_as!(
            MediaItem,
            r#"
            INSERT INTO media_items (
                title, speaker, date, description, scripture_reading,
                file_path, file_size, duration_seconds, video_codec, audio_codec,
                resolution, bitrate, thumbnail_path, thumbnail_generated_at,
                nfo_path, last_scanned
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
            ON CONFLICT (file_path) DO UPDATE SET
                title = EXCLUDED.title,
                speaker = EXCLUDED.speaker,
                date = EXCLUDED.date,
                description = EXCLUDED.description,
                scripture_reading = EXCLUDED.scripture_reading,
                file_size = EXCLUDED.file_size,
                duration_seconds = EXCLUDED.duration_seconds,
                video_codec = EXCLUDED.video_codec,
                audio_codec = EXCLUDED.audio_codec,
                resolution = EXCLUDED.resolution,
                bitrate = EXCLUDED.bitrate,
                nfo_path = EXCLUDED.nfo_path,
                last_scanned = EXCLUDED.last_scanned,
                updated_at = NOW()
            RETURNING id, title, speaker, date, description, scripture_reading,
                      file_path, file_size, duration_seconds, video_codec, audio_codec,
                      resolution, bitrate, thumbnail_path, thumbnail_generated_at,
                      nfo_path, last_scanned, created_at, updated_at
            "#,
            media_item.title,
            media_item.speaker,
            media_item.date,
            media_item.description,
            media_item.scripture_reading,
            media_item.file_path,
            media_item.file_size,
            media_item.duration_seconds,
            media_item.video_codec,
            media_item.audio_codec,
            media_item.resolution,
            media_item.bitrate,
            media_item.thumbnail_path,
            media_item.thumbnail_generated_at,
            media_item.nfo_path,
            media_item.last_scanned
        )
        .fetch_one(&self.pool)
        .await
        .map_err(|e| ApiError::Database(e.to_string()))?;

        Ok(saved)
    }

    async fn update_scan_status(&self, scan_path: &str, files_processed: i32, files_found: i32, errors: Vec<String>) -> Result<()> {
        sqlx::query!(
            r#"
            INSERT INTO media_scan_status (scan_path, files_found, files_processed, errors)
            VALUES ($1, $2, $3, $4)
            "#,
            scan_path,
            files_found,
            files_processed,
            &errors
        )
        .execute(&self.pool)
        .await
        .map_err(|e| ApiError::Database(e.to_string()))?;

        Ok(())
    }

    async fn parse_nfo_file(&self, nfo_path: &Path) -> Result<NFOMetadata> {
        let content = fs::read_to_string(nfo_path).map_err(|e| {
            ApiError::Internal(format!("Failed to read NFO file: {}", e))
        })?;

        let mut metadata = NFOMetadata::default();
        
        // Parse XML content
        if let Ok(doc) = roxmltree::Document::parse(&content) {
            let root = doc.root_element();
            
            // Handle both episodedetails and movie formats
            if root.tag_name().name() == "episodedetails" {
                metadata.title = root.children()
                    .find(|n| n.tag_name().name() == "title")
                    .and_then(|n| n.text())
                    .map(|s| s.to_string());
                    
                // Parse the title to extract speaker and date
                if let Some(ref title) = metadata.title {
                    let parsed = parse_media_title(title);
                    metadata.title = Some(parsed.title);
                    if metadata.speaker.is_none() {
                        metadata.speaker = parsed.speaker;
                    }
                    if metadata.date.is_none() {
                        metadata.date = parsed.date;
                    }
                }
                
                // Parse aired date
                if let Some(aired_text) = root.children()
                    .find(|n| n.tag_name().name() == "aired")
                    .and_then(|n| n.text()) {
                    metadata.date = chrono::NaiveDate::parse_from_str(aired_text, "%Y-%m-%d").ok();
                }
            } else if root.tag_name().name() == "movie" {
                metadata.title = root.children()
                    .find(|n| n.tag_name().name() == "title")
                    .and_then(|n| n.text())
                    .map(|s| {
                        // Remove .mp4 extension from title
                        let title = s.to_string();
                        if title.ends_with(".mp4") {
                            title[..title.len() - 4].to_string()
                        } else {
                            title
                        }
                    });
                    
                // Parse the title to extract speaker and date
                if let Some(ref title) = metadata.title {
                    let parsed = parse_media_title(title);
                    metadata.title = Some(parsed.title);
                    if metadata.speaker.is_none() {
                        metadata.speaker = parsed.speaker;
                    }
                    if metadata.date.is_none() {
                        metadata.date = parsed.date;
                    }
                }
            }
        }
        
        tracing::debug!("Parsed NFO metadata: title={:?}, speaker={:?}, date={:?}", 
            metadata.title, metadata.speaker, metadata.date);
            
        Ok(metadata)
    }

    /// Generate thumbnail for video file using GStreamer
    async fn generate_thumbnail(&self, video_path: &Path, media_id: &uuid::Uuid) -> Result<String> {
        // Create thumbnails directory if it doesn't exist
        let upload_dir = std::env::var("UPLOAD_DIR").unwrap_or_else(|_| "/opt/rtsda/church-api/uploads".to_string());
        let thumbnail_dir = format!("{}/thumbnails", upload_dir);
        
        if let Err(e) = fs::create_dir_all(&thumbnail_dir) {
            return Err(ApiError::Internal(format!("Failed to create thumbnail directory: {}", e)));
        }

        let thumbnail_filename = format!("{}.webp", media_id);
        let thumbnail_path = format!("{}/{}", thumbnail_dir, thumbnail_filename);
        
        use gstreamer::prelude::*;
        
        // Initialize GStreamer if not already done
        let _ = gstreamer::init();
        
        // Create pipeline for thumbnail generation
        let pipeline = gstreamer::Pipeline::new();
        
        // Create elements
        let filesrc = gstreamer::ElementFactory::make("filesrc")
            .property("location", video_path.to_str().unwrap())
            .build()
            .map_err(|e| ApiError::Internal(format!("Failed to create filesrc: {}", e)))?;
        
        let decodebin = gstreamer::ElementFactory::make("decodebin")
            .build()
            .map_err(|e| ApiError::Internal(format!("Failed to create decodebin: {}", e)))?;
        
        let videoconvert = gstreamer::ElementFactory::make("videoconvert")
            .build()
            .map_err(|e| ApiError::Internal(format!("Failed to create videoconvert: {}", e)))?;
        
        let videoscale = gstreamer::ElementFactory::make("videoscale")
            .build()
            .map_err(|e| ApiError::Internal(format!("Failed to create videoscale: {}", e)))?;
        
        // Create caps for high-quality retina display thumbnails (16:9 aspect ratio)
        let caps = gstreamer::Caps::builder("video/x-raw")
            .field("width", 1600i32)
            .field("height", 900i32)
            .build();
        
        let capsfilter = gstreamer::ElementFactory::make("capsfilter")
            .property("caps", &caps)
            .build()
            .map_err(|e| ApiError::Internal(format!("Failed to create capsfilter: {}", e)))?;
        
        // WebP encoder with high quality settings for retina displays
        let webpenc = gstreamer::ElementFactory::make("webpenc")
            .property("lossless", false)
            .property("quality", 95.0f32)  // Higher quality for better scaling
            .build()
            .map_err(|e| ApiError::Internal(format!("Failed to create webpenc: {}", e)))?;
        
        let filesink = gstreamer::ElementFactory::make("filesink")
            .property("location", &thumbnail_path)
            .build()
            .map_err(|e| ApiError::Internal(format!("Failed to create filesink: {}", e)))?;
        
        // Add elements to pipeline
        pipeline.add_many([
            &filesrc, &decodebin, &videoconvert, &videoscale, 
            &capsfilter, &webpenc, &filesink
        ]).map_err(|e| ApiError::Internal(format!("Failed to add elements: {}", e)))?;
        
        // Link static elements
        filesrc.link(&decodebin)
            .map_err(|e| ApiError::Internal(format!("Failed to link filesrc to decodebin: {}", e)))?;
        
        gstreamer::Element::link_many([&videoconvert, &videoscale, &capsfilter, &webpenc, &filesink])
            .map_err(|e| ApiError::Internal(format!("Failed to link processing chain: {}", e)))?;
        
        // Handle dynamic pad from decodebin
        let videoconvert_clone = videoconvert.clone();
        decodebin.connect_pad_added(move |_element, pad| {
            let _pad_name = pad.name();
            // Only connect video pads
            if let Some(caps) = pad.current_caps() {
                if let Some(structure) = caps.structure(0) {
                    let name = structure.name();
                    if name.starts_with("video/") {
                        let sink_pad = videoconvert_clone.static_pad("sink").unwrap();
                        if !sink_pad.is_linked() {
                            let _ = pad.link(&sink_pad);
                        }
                    }
                }
            }
        });
        
        // Set pipeline to paused for seeking
        pipeline.set_state(gstreamer::State::Paused)
            .map_err(|e| ApiError::Internal(format!("Failed to pause pipeline: {}", e)))?;
        
        // Wait for preroll
        let bus = pipeline.bus().unwrap();
        let timeout = gstreamer::ClockTime::from_seconds(10);
        if let Some(msg) = bus.timed_pop_filtered(Some(timeout), &[gstreamer::MessageType::AsyncDone, gstreamer::MessageType::Error]) {
            match msg.view() {
                gstreamer::MessageView::Error(err) => {
                    return Err(ApiError::Internal(format!("Pipeline error during preroll: {}", err.error())));
                }
                _ => {}
            }
        }
        
        // Seek to 1 minute (60 seconds) for thumbnail
        let seek_time = gstreamer::ClockTime::from_seconds(60);
        pipeline.seek_simple(
            gstreamer::SeekFlags::FLUSH | gstreamer::SeekFlags::KEY_UNIT,
            seek_time
        ).map_err(|_| ApiError::Internal("Failed to seek for thumbnail".to_string()))?;
        
        // Set to playing to generate one frame
        pipeline.set_state(gstreamer::State::Playing)
            .map_err(|e| ApiError::Internal(format!("Failed to start pipeline: {}", e)))?;
        
        // Wait a short time for the frame to be processed
        tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
        
        // Stop the pipeline
        pipeline.set_state(gstreamer::State::Null)
            .map_err(|e| ApiError::Internal(format!("Failed to stop pipeline: {}", e)))?;
        
        // Verify thumbnail was created
        if !std::path::Path::new(&thumbnail_path).exists() {
            return Err(ApiError::Internal("Thumbnail file was not created".to_string()));
        }
        
        tracing::info!("Generated thumbnail for {} using GStreamer: {}", video_path.display(), thumbnail_path);
        Ok(thumbnail_path)
    }

    /// Update thumbnail path in database
    async fn update_thumbnail_path(&self, media_id: uuid::Uuid, thumbnail_path: &str) -> Result<MediaItem> {
        let updated_item = sqlx::query_as!(
            MediaItem,
            r#"
            UPDATE media_items 
            SET thumbnail_path = $1, thumbnail_generated_at = NOW(), updated_at = NOW()
            WHERE id = $2
            RETURNING id, title, speaker, date, description, scripture_reading,
                      file_path, file_size, duration_seconds, video_codec, audio_codec,
                      resolution, bitrate, thumbnail_path, thumbnail_generated_at,
                      nfo_path, last_scanned, created_at, updated_at
            "#,
            thumbnail_path,
            media_id
        )
        .fetch_one(&self.pool)
        .await
        .map_err(|e| ApiError::Database(e.to_string()))?;

        Ok(updated_item)
    }
}

#[derive(Debug, Default)]
struct NFOMetadata {
    pub title: Option<String>,
    pub speaker: Option<String>,
    pub date: Option<chrono::NaiveDate>,
    pub description: Option<String>,
    pub scripture_reading: Option<String>,
}

#[derive(Debug)]
struct MediaInfo {
    pub duration_seconds: Option<i32>,
    pub video_codec: Option<String>,
    pub audio_codec: Option<String>,
    pub resolution: Option<String>,
    pub bitrate: Option<i32>,
}