use base64::Engine;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::BufRead;
use std::process::{Command, Stdio};
use std::sync::Mutex;
use tauri::{Emitter, Runtime, State};
use tauri_plugin_shell::ShellExt;
use tempfile::TempDir;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Failed to execute FFmpeg: {0}")]
    FFmpegError(String),
    #[error("IO error: {0}")]
    IoError(#[from] std::io::Error),
    #[error("Gemini API error: {0}")]
    GeminiError(String),
    #[error("Image processing error: {0}")]
    ImageError(String),
    #[error("HTTP error: {0}")]
    HttpError(#[from] reqwest::Error),
}

impl serde::Serialize for Error {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
    {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

#[derive(Default)]
pub struct ProgressState(pub Mutex<f32>);

#[derive(Debug, Deserialize)]
pub struct MergeArgs {
    pub video_path: String,
    pub subtitle_path: String,
    pub add_black_bar: bool,
    pub resize_to_720p: bool,
}

#[derive(Debug, Deserialize)]
pub struct SelectedFrame {
    pub path: String,
    pub time: u32,
    pub base64_image: String,
}

#[derive(Debug, Deserialize)]
pub struct TranscriptionArgs {
    pub video_path: String,
    pub subtitle_path: Option<String>,
    pub selected_frames: Vec<SelectedFrame>,
    pub api_key: String,
    pub visual_prompt: String,
    pub subtitle_prompt: String,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TranscriptionResult {
    pub narrative: String,
}

#[derive(Debug, Serialize)]
pub struct Thumbnail {
    pub path: String,
    pub time: u32,
}

pub struct ThumbnailState {
    temp_dir: Option<TempDir>,
}

impl Default for ThumbnailState {
    fn default() -> Self {
        Self { temp_dir: None }
    }
}

impl ThumbnailState {
    pub fn set_temp_dir(&mut self, dir: TempDir) {
        self.temp_dir = Some(dir);
    }

    pub fn get_temp_dir(&self) -> Option<&TempDir> {
        self.temp_dir.as_ref()
    }
}

#[derive(Debug, Serialize, Clone)]
pub struct ProgressUpdate {
    pub progress: f32,
}

pub async fn check_ffmpeg() -> Result<(), Error> {
    println!("Checking FFmpeg installation...");
    let output = Command::new("/opt/homebrew/bin/ffmpeg")
        .arg("-version")
        .output()
        .map_err(|e| {
            println!("FFmpeg check failed: {}", e);
            Error::FFmpegError(format!(
                "FFmpeg is not installed or not found in PATH: {}",
                e
            ))
        })?;

    if !output.status.success() {
        return Err(Error::FFmpegError("FFmpeg check failed".to_string()));
    }

    Ok(())
}

pub async fn get_video_duration(video_path: &str) -> Result<f32, Error> {
    let output = Command::new("/opt/homebrew/bin/ffprobe")
        .args([
            "-v",
            "error",
            "-show_entries",
            "format=duration",
            "-of",
            "default=noprint_wrappers=1:nokey=1",
            video_path,
        ])
        .output()
        .map_err(|e| Error::FFmpegError(format!("Failed to run ffprobe: {}", e)))?;

    if !output.status.success() {
        return Err(Error::FFmpegError(
            "Failed to get video duration".to_string(),
        ));
    }

    String::from_utf8_lossy(&output.stdout)
        .trim()
        .parse::<f32>()
        .map_err(|e| Error::FFmpegError(format!("Failed to parse duration: {}", e)))
}

async fn extract_frame(video_path: &str, time: u32, output_path: &str) -> Result<(), Error> {
    let output = Command::new("/opt/homebrew/bin/ffmpeg")
        .args([
            "-ss",
            &time.to_string(),
            "-i",
            video_path,
            "-vframes",
            "1",
            "-q:v",
            "2",
            "-y",
            output_path,
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .map_err(|e| Error::FFmpegError(format!("Failed to execute FFmpeg: {}", e)))?;

    if !output.status.success() {
        let error = String::from_utf8_lossy(&output.stderr);
        return Err(Error::FFmpegError(format!(
            "Frame extraction failed: {}",
            error
        )));
    }

    if !std::path::Path::new(output_path).exists() {
        return Err(Error::FFmpegError(format!(
            "Output file was not created: {}",
            output_path
        )));
    }

    Ok(())
}

async fn query_gemini<R: Runtime>(
    app_handle: &tauri::AppHandle<R>,
    api_key: &str,
    prompt: &str,
    image_path: Option<&str>,
) -> Result<String, Error> {
    println!("\n=== Gemini API Request ===");
    println!("Prompt: {}", prompt);
    if let Some(path) = image_path {
        println!("Image Path: {}", path);
    }

    let sidecar = app_handle
        .shell()
        .sidecar("gemini-query")
        .map_err(|e| Error::GeminiError(format!("Failed to get sidecar: {}", e)))?;

    let mut command = sidecar.arg(api_key).arg(prompt);

    if let Some(path) = image_path {
        command = command.arg(path);
    }

    println!("\nExecuting sidecar command");
    let output = command
        .output()
        .await
        .map_err(|e| Error::GeminiError(format!("Failed to execute Gemini query: {}", e)))?;

    let response_text = String::from_utf8_lossy(&output.stdout);

    // Try to parse the response as JSON
    match serde_json::from_str::<serde_json::Value>(&response_text) {
        Ok(response) => {
            if response["success"].as_bool().unwrap_or(false) {
                let result = response["text"].as_str().unwrap_or("").to_string();
                println!("\n=== Gemini API Success ===");
                println!("Response length: {} characters", result.len());
                Ok(result)
            } else {
                let error = response["error"]
                    .as_str()
                    .unwrap_or("Unknown error")
                    .to_string();
                println!("\n=== Gemini API Error ===");
                println!("Error: {}", error);
                Err(Error::GeminiError(error))
            }
        }
        Err(e) => {
            println!("\n=== Gemini API Parse Error ===");
            println!("Raw output: {}", response_text);
            println!("Parse error: {}", e);
            Err(Error::GeminiError(format!(
                "Failed to parse response: {}",
                e
            )))
        }
    }
}

pub async fn test_gemini_api_impl<R: Runtime>(
    app_handle: &tauri::AppHandle<R>,
    api_key: &str,
) -> Result<(), Error> {
    query_gemini(app_handle, api_key, "Test connection", None).await?;
    Ok(())
}

pub async fn extract_thumbnails_impl(
    video_path: &str,
    interval: u32,
    thumbnail_state: State<'_, Mutex<ThumbnailState>>,
) -> Result<Vec<Thumbnail>, Error> {
    // Create a new temporary directory
    let temp_dir = TempDir::new().map_err(|e| Error::IoError(e))?;
    println!(
        "Extracting thumbnails to temporary directory: {:?}",
        temp_dir.path()
    );

    let duration = get_video_duration(video_path).await? as u32;
    let mut thumbnails = Vec::new();

    // Process all frames before moving temp_dir into state
    for time in (0..duration).step_by(interval as usize) {
        let output_path = temp_dir.path().join(format!("thumb_{}.jpg", time));
        match extract_frame(video_path, time, output_path.to_str().unwrap()).await {
            Ok(_) => {
                println!("Successfully extracted frame at time {}", time);
                thumbnails.push(Thumbnail {
                    path: output_path.to_str().unwrap().to_string(),
                    time,
                });
            }
            Err(e) => {
                println!("Failed to extract frame at time {}: {}", time, e);
                continue;
            }
        }
    }

    if thumbnails.is_empty() {
        return Err(Error::FFmpegError(
            "Failed to extract any thumbnails".to_string(),
        ));
    }

    // Store the TempDir in the state only after we're done using it
    thumbnail_state.lock().unwrap().set_temp_dir(temp_dir);
    Ok(thumbnails)
}

pub async fn process_transcription_impl<R: Runtime>(
    app_handle: &tauri::AppHandle<R>,
    args: TranscriptionArgs,
) -> Result<TranscriptionResult, Error> {
    println!("\n=== Starting Transcription Process ===");
    println!("Video Path: {}", args.video_path);
    if let Some(subtitle_path) = &args.subtitle_path {
        println!("Subtitle Path: {}", subtitle_path);
    }
    println!("Number of selected frames: {}", args.selected_frames.len());

    let temp_dir = TempDir::new().map_err(|e| Error::IoError(e))?;

    // Process each frame and collect descriptions
    let mut frame_descriptions = Vec::new();
    for (index, frame) in args.selected_frames.iter().enumerate() {
        println!("\nProcessing frame {} at time {}", index + 1, frame.time);

        // Save base64 image to temp file
        let frame_path = temp_dir.path().join(format!("frame_{}.jpg", frame.time));
        let image_data = base64::engine::general_purpose::STANDARD
            .decode(&frame.base64_image)
            .map_err(|e| Error::ImageError(format!("Failed to decode base64 image: {}", e)))?;
        fs::write(&frame_path, image_data).map_err(|e| Error::IoError(e))?;

        let frame_prompt = format!("{}\n\nTimestamp: {}", args.visual_prompt, frame.time);

        let description = query_gemini(
            app_handle,
            &args.api_key,
            &frame_prompt,
            Some(frame_path.to_str().unwrap()),
        )
        .await?;
        frame_descriptions.push((frame.time, description));
    }

    // Format scene descriptions with timestamps in MM:SS format
    let scene_descriptions = frame_descriptions
        .iter()
        .map(|(time, desc)| {
            let minutes = time / 60;
            let seconds = time % 60;
            format!("[{:02}:{:02}] {}", minutes, seconds, desc)
        })
        .collect::<Vec<_>>()
        .join("\n\n");

    // Process subtitle content if provided
    let narrative = if let Some(subtitle_path) = &args.subtitle_path {
        println!("\nReading and parsing subtitle content...");
        let subtitle_content = fs::read_to_string(subtitle_path).map_err(|e| Error::IoError(e))?;
        println!(
            "Subtitle content length: {} characters",
            subtitle_content.len()
        );

        // Parse subtitle timestamps and content, handling both SRT and ASS formats
        let mut subtitle_lines = Vec::new();
        let lines: Vec<&str> = subtitle_content.lines().collect();

        // Check if it's an ASS file
        if subtitle_path.to_lowercase().ends_with(".ass") {
            let mut in_events = false;
            for line in lines {
                if line.starts_with("[Events]") {
                    in_events = true;
                    continue;
                }
                if in_events && line.starts_with("Dialogue:") {
                    let parts: Vec<&str> = line.split(',').collect();
                    if parts.len() >= 10 {
                        let start_time = parts[1].trim();
                        // Combine all remaining parts as the text, skipping style and positioning info
                        let text = parts[9..].join(",").trim().to_string();
                        if !text.is_empty() {
                            subtitle_lines.push((start_time.to_string(), text));
                        }
                    }
                }
            }
        } else {
            // SRT format parsing
            let mut i = 0;
            while i < lines.len() {
                if i + 3 <= lines.len() && lines[i + 1].contains("-->") {
                    let timestamp_parts: Vec<&str> = lines[i + 1].split(" --> ").collect();
                    if timestamp_parts.len() == 2 {
                        let start_time = timestamp_parts[0].trim();
                        let mut text = lines[i + 2].trim().to_string();

                        // Collect multi-line subtitle text
                        let mut j = i + 3;
                        while j < lines.len()
                            && !lines[j].trim().is_empty()
                            && !lines[j].contains("-->")
                        {
                            text.push_str(" ");
                            text.push_str(lines[j].trim());
                            j += 1;
                        }

                        subtitle_lines.push((start_time.to_string(), text));
                        i = j;
                    }
                }
                i += 1;
            }
        }

        // Sort subtitles by timestamp
        subtitle_lines.sort_by(|a, b| a.0.cmp(&b.0));

        // Format subtitle content with timestamps
        let formatted_subtitles = subtitle_lines
            .iter()
            .map(|(time, text)| format!("[{}] {}", time, text))
            .collect::<Vec<_>>()
            .join("\n");

        let narrative_prompt = format!(
            "{}\n\nScene Descriptions:\n{}\n\nSubtitle Content with Timestamps:\n{}",
            args.subtitle_prompt, scene_descriptions, formatted_subtitles
        );

        // Process the subtitle content with scene descriptions
        query_gemini(app_handle, &args.api_key, &narrative_prompt, None).await?
    } else {
        // If no subtitle file, just format the scene descriptions as a narrative
        let narrative_prompt = format!("Create a flowing narrative from these scene descriptions, maintaining their chronological order and focusing on spatial relationships and visual details that would be helpful for DeafBlind readers:\n\n{}", scene_descriptions);
        query_gemini(app_handle, &args.api_key, &narrative_prompt, None).await?
    };

    println!("\n=== Final Transcription Result ===");
    println!("\nNarrative:");
    println!("{}", narrative);

    // Create the result struct
    let result = TranscriptionResult {
        narrative: narrative.clone(),
    };

    // Log the final result structure
    println!("\nReturning transcription result with:");
    println!("- Narrative length: {}", result.narrative.len());
    println!("\nFinal TranscriptionResult struct:");
    println!("{{");
    println!("  \"narrative\": \"{}\"", result.narrative);
    println!("}}");

    Ok(result)
}

pub async fn merge_video_subtitle_impl<R: Runtime>(
    args: MergeArgs,
    progress_state: State<'_, ProgressState>,
    app_handle: tauri::AppHandle<R>,
) -> Result<String, Error> {
    let output_path = format!("{}-subbed.mp4", args.video_path.trim_end_matches(".mp4"));

    let total_duration = get_video_duration(&args.video_path).await?;
    println!("Video duration: {} seconds", total_duration);

    let mut filters = Vec::new();
    if args.add_black_bar {
        filters.push("pad=iw:ih+170:0:0:black".to_string());
    }
    if args.resize_to_720p {
        filters.push("scale=-1:720".to_string());
    }

    let subtitle_path = args
        .subtitle_path
        .replace("'", "'\\''")
        .replace(",", "\\,")
        .replace(" ", "\\ ");
    filters.push(format!("subtitles='{}'", subtitle_path));
    filters.push("format=nv12|qsv".to_string());

    let filter_chain = filters.join(",");
    println!("Filter chain: {}", filter_chain);

    check_ffmpeg().await?;

    println!(
        "Starting FFmpeg process with paths: video={}, subtitle={}",
        args.video_path, args.subtitle_path
    );
    let mut command = Command::new("/opt/homebrew/bin/ffmpeg");
    command
        .arg("-y")
        .arg("-hwaccel")
        .arg("videotoolbox")
        .arg("-i")
        .arg(&args.video_path)
        .arg("-vf")
        .arg(&filter_chain)
        .arg("-c:v")
        .arg("h264_videotoolbox")
        .arg("-b:v")
        .arg("5000k")
        .arg("-progress")
        .arg("pipe:1")
        .arg("-loglevel")
        .arg("error")
        .arg(&output_path)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());

    println!("FFmpeg command: {:?}", command);
    let mut child = command.spawn().map_err(|e| {
        println!("Failed to spawn FFmpeg: {}", e);
        Error::FFmpegError(format!("Failed to start FFmpeg process: {}", e))
    })?;
    let stdout = child.stdout.take().unwrap();
    let mut reader = std::io::BufReader::new(stdout);
    let mut line = String::new();

    let mut last_progress = 0.0;
    while let Ok(bytes) = reader.read_line(&mut line) {
        if bytes == 0 {
            break;
        }

        if line.starts_with("out_time=") {
            if let Some(time_str) = line.split('=').nth(1) {
                let parts: Vec<&str> = time_str.trim().split(':').collect();
                if parts.len() == 3 {
                    if let (Ok(hours), Ok(minutes), Ok(seconds)) = (
                        parts[0].parse::<f32>(),
                        parts[1].parse::<f32>(),
                        parts[2].parse::<f32>(),
                    ) {
                        let current_seconds = hours * 3600.0 + minutes * 60.0 + seconds;
                        let progress = ((current_seconds / total_duration) * 100.0)
                            .min(100.0)
                            .max(0.0);

                        if progress - last_progress >= 1.0 || progress == 100.0 {
                            *progress_state.0.lock().unwrap() = progress;
                            if let Err(e) = app_handle.emit("progress", ProgressUpdate { progress })
                            {
                                println!("Failed to emit progress: {}", e);
                            }
                            println!("Progress: {:.1}%", progress);
                            last_progress = progress;
                        }
                    }
                }
            }
        }
        line.clear();
    }

    let status = child.wait()?;
    if !status.success() {
        let error_msg = format!("FFmpeg process failed with status: {}", status);
        println!("{}", error_msg);
        return Err(Error::FFmpegError(error_msg));
    }
    println!("FFmpeg process completed successfully");

    let _ = Command::new("open").arg("-R").arg(&output_path).spawn();

    Ok(output_path)
}
