use anyhow::{Context, Result, anyhow};
use std::fs;
use std::path::{Path, PathBuf};

use crate::ui::prelude::{Level, emit};

use super::audio_preprocessing::{PreprocessorType, create_preprocessor, parse_preprocessor_type};
use super::cli::{ConvertArgs, TranscribeArgs};
use super::config::{VideoConfig, VideoDirectories, VideoProjectPaths};
use super::markdown::{MarkdownMetadata, build_markdown};
use super::transcribe::handle_transcribe;
use super::transcript::parse_whisper_json;
use super::utils::{canonicalize_existing, compute_file_hash};

pub async fn handle_convert(args: ConvertArgs) -> Result<()> {
    emit(
        Level::Info,
        "video.convert.start",
        &format!("Analyzing video {}...", args.video.display()),
        None,
    );
    let video_path = canonicalize_existing(&args.video)?;
    let video_hash = compute_file_hash(&video_path)?;

    let directories = VideoDirectories::new()?;
    let project_paths = directories.project_paths(&video_hash);
    project_paths.ensure_directories()?;

    let output_path = determine_output_path(args.out_file.clone(), &video_path)?;

    if output_path.exists() && !args.force {
        anyhow::bail!(
            "Markdown file already exists at {}. Use --force to overwrite.",
            output_path.display()
        );
    }

    // Step 1: Ensure we have a transcript
    let transcript_path = ensure_transcript(
        &video_path,
        &directories,
        &project_paths,
        args.transcript.as_ref(),
        &args,
    )
    .await?;

    // Step 2: Generate markdown output
    generate_markdown_output(
        &video_path,
        &video_hash,
        &transcript_path,
        &output_path,
        &project_paths,
    )?;

    emit(
        Level::Success,
        "video.convert.success",
        &format!("Generated markdown at {}", output_path.display()),
        None,
    );

    Ok(())
}

/// Ensures a transcript exists for the video, generating one if needed.
/// Returns the path to the transcript file.
async fn ensure_transcript(
    video_path: &Path,
    directories: &VideoDirectories,
    project_paths: &VideoProjectPaths,
    provided_transcript: Option<&PathBuf>,
    args: &ConvertArgs,
) -> Result<PathBuf> {
    let cached_transcript_path = project_paths.transcript_cache_path().to_path_buf();

    // If user provided a transcript, use it
    if let Some(provided) = provided_transcript {
        let provided_path = canonicalize_existing(provided)?;
        copy_transcript(&provided_path, &cached_transcript_path)?;
        return Ok(cached_transcript_path);
    }

    // If transcript already cached, use it
    if cached_transcript_path.exists() {
        return Ok(cached_transcript_path);
    }

    // Generate transcript
    let audio_source = get_audio_source(video_path, args).await?;

    emit(
        Level::Info,
        "video.convert.transcribe",
        "Transcribing audio (this may take a while)...",
        None,
    );

    handle_transcribe(TranscribeArgs {
        video: audio_source.clone(),
        compute_type: "int8".to_string(),
        device: "cpu".to_string(),
        model: None,
        vad_method: "silero".to_string(),
        force: false,
    })?;

    // If we transcribed processed audio, relocate transcript to video's cache
    relocate_transcript_if_needed(
        video_path,
        &audio_source,
        directories,
        &cached_transcript_path,
    )?;

    if !cached_transcript_path.exists() {
        anyhow::bail!(
            "Transcript not found at {} even after attempting transcription.",
            cached_transcript_path.display()
        );
    }

    Ok(cached_transcript_path)
}

/// Gets the audio source for transcription using the configured preprocessor.
async fn get_audio_source(video_path: &Path, args: &ConvertArgs) -> Result<PathBuf> {
    // Skip preprocessing entirely if requested
    if args.no_preprocess {
        return Ok(video_path.to_path_buf());
    }

    let config = VideoConfig::load()?;

    // Determine preprocessor type: CLI flag > config
    let preprocessor_type = match &args.preprocessor {
        Some(s) => parse_preprocessor_type(s)?,
        None => config.preprocessor.clone(),
    };

    // Skip if preprocessor is None
    if preprocessor_type == PreprocessorType::None {
        return Ok(video_path.to_path_buf());
    }

    let preprocessor = create_preprocessor(&preprocessor_type, &config);

    if !preprocessor.is_available() {
        emit(
            Level::Warn,
            "video.convert.preprocessor_unavailable",
            &format!(
                "Preprocessor '{}' is not available. Falling back to original video.",
                preprocessor.name()
            ),
            None,
        );
        return Ok(video_path.to_path_buf());
    }

    match preprocessor.process(video_path, args.force).await {
        Ok(result) => Ok(result.output_path),
        Err(e) => {
            emit(
                Level::Warn,
                "video.convert.preprocess_failed",
                &format!(
                    "Audio preprocessing failed: {}. Falling back to original video.",
                    e
                ),
                None,
            );
            Ok(video_path.to_path_buf())
        }
    }
}

/// Relocates transcript from audio's cache to video's cache if needed.
fn relocate_transcript_if_needed(
    video_path: &Path,
    audio_source: &Path,
    directories: &VideoDirectories,
    cached_transcript_path: &Path,
) -> Result<()> {
    if audio_source == video_path {
        return Ok(());
    }

    let audio_hash = compute_file_hash(audio_source)?;
    let audio_project_paths = directories.project_paths(&audio_hash);
    let generated_transcript = audio_project_paths.transcript_cache_path();

    if generated_transcript.exists() {
        emit(
            Level::Debug,
            "video.convert.relocate",
            &format!(
                "Moving transcript from {} to {}",
                generated_transcript.display(),
                cached_transcript_path.display()
            ),
            None,
        );
        copy_transcript(generated_transcript, cached_transcript_path)?;
    }

    Ok(())
}

/// Generates the markdown output file from a transcript.
fn generate_markdown_output(
    video_path: &Path,
    video_hash: &str,
    transcript_path: &Path,
    output_path: &Path,
    project_paths: &VideoProjectPaths,
) -> Result<()> {
    let markdown_dir = output_path.parent().unwrap_or_else(|| Path::new("."));
    let subtitle_dir = markdown_dir.join("insvideodata");
    let subtitle_output_path = subtitle_dir.join(format!("{video_hash}.json"));
    let relative_subtitle_path = Path::new("./insvideodata").join(format!("{video_hash}.json"));

    copy_transcript(transcript_path, &subtitle_output_path)?;

    let transcript_contents = fs::read_to_string(transcript_path)
        .with_context(|| format!("Failed to read transcript at {}", transcript_path.display()))?;

    let cues = parse_whisper_json(&transcript_contents)?;

    let video_name = video_path
        .file_name()
        .map(|name| name.to_string_lossy().into_owned())
        .context("Video file name is not valid UTF-8")?;

    // Compute relative path from markdown directory to video file
    let relative_video_path = pathdiff::diff_paths(video_path, markdown_dir).ok_or_else(|| {
        anyhow!(
            "Failed to compute relative path from {} to {}",
            markdown_dir.display(),
            video_path.display()
        )
    })?;

    let metadata = MarkdownMetadata {
        video_hash,
        video_name: video_name.as_str(),
        video_source: &relative_video_path,
        transcript_source: &relative_subtitle_path,
    };

    let markdown = build_markdown(&cues, &metadata);

    if let Some(parent) = output_path.parent() {
        fs::create_dir_all(parent)
            .with_context(|| format!("Failed to create output directory {}", parent.display()))?;
    }

    fs::write(output_path, markdown.as_bytes())
        .with_context(|| format!("Failed to write markdown file to {}", output_path.display()))?;

    write_metadata_file(
        project_paths,
        video_hash,
        video_path,
        &subtitle_output_path,
        output_path,
    )?;

    emit(
        Level::Info,
        "video.convert.subtitles",
        &format!("Stored subtitles at {}", subtitle_output_path.display()),
        None,
    );

    Ok(())
}

fn copy_transcript(src: &Path, dest: &Path) -> Result<()> {
    if src == dest {
        return Ok(());
    }
    if dest.exists() {
        return Ok(());
    }
    if let Some(parent) = dest.parent() {
        fs::create_dir_all(parent).with_context(|| {
            format!("Failed to create transcript directory {}", parent.display())
        })?;
    }
    fs::copy(src, dest).with_context(|| {
        format!(
            "Failed to copy transcript from {} to {}",
            src.display(),
            dest.display()
        )
    })?;
    Ok(())
}

fn determine_output_path(output: Option<PathBuf>, video_path: &Path) -> Result<PathBuf> {
    match output {
        Some(path) => Ok(path),
        None => {
            // Default to <videoname>.md next to the video file
            let video_stem = video_path
                .file_stem()
                .context("Video file has no stem")?
                .to_string_lossy();
            let mut default_output = video_path.to_path_buf();
            default_output.set_file_name(format!("{}.video.md", video_stem));
            Ok(default_output)
        }
    }
}

fn write_metadata_file(
    project_paths: &VideoProjectPaths,
    video_hash: &str,
    video_path: &Path,
    transcript_path: &Path,
    markdown_path: &Path,
) -> Result<()> {
    let timestamp = chrono::Utc::now().to_rfc3339();
    let contents = format!(
        "video_hash: {hash}\nvideo_source: {video}\ntranscript_source: {transcript}\nmarkdown: {markdown}\nupdated_at: '{timestamp}'\n",
        hash = yaml_quote(video_hash),
        video = yaml_quote_path(video_path),
        transcript = yaml_quote_path(transcript_path),
        markdown = yaml_quote_path(markdown_path),
    );

    fs::write(project_paths.metadata_path(), contents).with_context(|| {
        format!(
            "Failed to write metadata file to {}",
            project_paths.metadata_path().display()
        )
    })?;
    Ok(())
}

fn yaml_quote(value: &str) -> String {
    if value.is_empty() {
        "''".to_string()
    } else {
        format!("'{}'", value.replace('\'', "''"))
    }
}

fn yaml_quote_path(path: &Path) -> String {
    yaml_quote(&path.to_string_lossy())
}
