//! Internal flatpak list generation for fast menu loading
//!
//! Uses local appstream metadata (~15x faster than `flatpak remote-ls`)
//! with fallback to remote-ls if appstream is unavailable.
//! Outputs apps incrementally for streaming fzf integration.

use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::Command;

use crate::menu_utils::{FzfPreview, StreamingMenuItem};
use crate::preview::{PreviewId, preview_command};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlatpakSelectionPayload {
    pub app_id: String,
    pub remote: Option<String>,
    pub installation: Option<String>,
}

/// Generate and print the flatpak app list to stdout
/// If keyword is provided, only matching apps are printed
/// Apps are printed incrementally as they're found (streaming)
pub fn generate_and_print_list(keyword: Option<&str>) -> Result<()> {
    eprintln!("Using local appstream cache (fast)");

    let keyword_lower = keyword.map(|s| s.to_lowercase());
    let mut printed_ids: HashSet<String> = HashSet::new();

    for path in get_appstream_paths() {
        let remote = appstream_remote_name(&path);
        let installation = appstream_installation(&path);
        parse_and_print_streaming(
            &path,
            installation.as_deref(),
            remote.as_deref(),
            keyword_lower.as_deref(),
            &mut printed_ids,
        )?;
    }

    if printed_ids.is_empty() {
        eprintln!("No local matches, fetching from remote...");
        fallback_to_remote_ls(keyword)?;
    }

    Ok(())
}

pub fn generate_and_print_installed_list() -> Result<()> {
    let output = Command::new("flatpak")
        .args([
            "list",
            "--app",
            "--columns=application,name,version,origin,size",
        ])
        .output()?;

    if !output.status.success() {
        return Ok(());
    }

    for line in String::from_utf8_lossy(&output.stdout).lines() {
        if line.trim().is_empty() {
            continue;
        }
        let mut parts = line.split('\t');
        let id = parts.next().unwrap_or_default();
        let name = parts.next().unwrap_or_default();
        let version = parts.next().unwrap_or_default();
        let origin = parts.next().unwrap_or_default();
        let size = parts.next().unwrap_or_default();
        let tail = [version, origin, size]
            .into_iter()
            .filter(|part| !part.is_empty())
            .collect::<Vec<_>>()
            .join("\t");
        print_flatpak_row(id, name, &tail, Some(origin), None)?;
    }

    Ok(())
}

/// Parse appstream file and print matching apps incrementally
fn parse_and_print_streaming(
    path: &PathBuf,
    installation: Option<&str>,
    remote: Option<&str>,
    keyword: Option<&str>,
    printed_ids: &mut HashSet<String>,
) -> Result<()> {
    let output = Command::new("zcat").arg(path).output()?;

    if !output.status.success() {
        return Ok(());
    }

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

    let mut current_id = String::new();
    let mut current_name = String::new();
    let mut current_summary = String::new();
    let mut in_desktop_component = false;

    for line in content.lines() {
        let line = line.trim();

        if line.contains("<component type=\"desktop-application\"") {
            if in_desktop_component && !current_id.is_empty() && !current_name.is_empty() {
                print_app_if_matches(
                    &current_id,
                    &current_name,
                    &current_summary,
                    installation,
                    remote,
                    keyword,
                    printed_ids,
                );
            }
            in_desktop_component = true;
            current_id.clear();
            current_name.clear();
            current_summary.clear();
            continue;
        }

        if !in_desktop_component {
            continue;
        }

        if line.contains("</component>") {
            let has_nested_element = line.contains("<developer>")
                || line.contains("<project>")
                || line.contains("<provides>");

            if !has_nested_element {
                if current_id.is_empty()
                    && let Some(id) = extract_tag_content(line, "id")
                {
                    current_id = id;
                }
                if current_name.is_empty()
                    && let Some(name) = extract_tag_content(line, "name")
                {
                    current_name = name;
                }
                if current_summary.is_empty()
                    && let Some(summary) = extract_tag_content(line, "summary")
                {
                    current_summary = summary;
                }
            }

            if !current_id.is_empty() && !current_name.is_empty() {
                print_app_if_matches(
                    &current_id,
                    &current_name,
                    &current_summary,
                    installation,
                    remote,
                    keyword,
                    printed_ids,
                );
            }
            in_desktop_component = false;
            continue;
        }

        if current_id.is_empty()
            && let Some(id) = extract_tag_content(line, "id")
        {
            current_id = id;
            continue;
        }

        if current_name.is_empty()
            && let Some(name) = extract_tag_content(line, "name")
        {
            current_name = name;
            continue;
        }

        if current_summary.is_empty()
            && let Some(summary) = extract_tag_content(line, "summary")
        {
            current_summary = summary;
            continue;
        }
    }

    if in_desktop_component && !current_id.is_empty() && !current_name.is_empty() {
        print_app_if_matches(
            &current_id,
            &current_name,
            &current_summary,
            installation,
            remote,
            keyword,
            printed_ids,
        );
    }

    Ok(())
}

/// Print app if it matches the keyword filter and hasn't been printed yet
fn print_app_if_matches(
    id: &str,
    name: &str,
    summary: &str,
    installation: Option<&str>,
    remote: Option<&str>,
    keyword: Option<&str>,
    printed_ids: &mut HashSet<String>,
) {
    if printed_ids.contains(id) {
        return;
    }

    if let Some(kw) = keyword {
        let kw_lower = kw.to_lowercase();
        let id_matches = id.to_lowercase().contains(&kw_lower);
        let name_matches = name.to_lowercase().contains(&kw_lower);
        let summary_matches = summary.to_lowercase().contains(&kw_lower);

        if !id_matches && !name_matches && !summary_matches {
            return;
        }
    }

    printed_ids.insert(id.to_string());
    let _ = print_flatpak_row(id, name, summary, remote, installation);
}

/// Extract text content between XML tags
fn extract_tag_content(line: &str, tag: &str) -> Option<String> {
    let open_tag = format!("<{}>", tag);
    let close_tag = format!("</{}>", tag);

    if let Some(start) = line.find(&open_tag)
        && let Some(end) = line.find(&close_tag)
    {
        let content = &line[start + open_tag.len()..end];
        if content.contains("xml:lang") {
            return None;
        }
        let trimmed = content.trim();
        if !trimmed.is_empty() && !trimmed.starts_with('<') {
            return Some(trimmed.to_string());
        }
    }
    None
}

/// Get all appstream.xml.gz paths from both user and system installations
fn get_appstream_paths() -> Vec<PathBuf> {
    let mut paths = Vec::new();

    if let Some(data_dir) = dirs::data_dir() {
        let user_appstream = data_dir.join("flatpak/appstream");
        paths.extend(find_appstream_files(&user_appstream));
    }

    paths.extend(find_appstream_files(&PathBuf::from(
        "/var/lib/flatpak/appstream",
    )));

    paths
}

/// Find appstream.xml.gz files in the given directory
fn find_appstream_files(base_dir: &PathBuf) -> Vec<PathBuf> {
    let mut files = Vec::new();

    if !base_dir.exists() {
        return files;
    }

    if let Ok(entries) = fs::read_dir(base_dir) {
        for entry in entries.flatten() {
            let remote_dir = entry.path();
            if remote_dir.is_dir() {
                let appstream_path = remote_dir.join("x86_64/active/appstream.xml.gz");
                if appstream_path.exists() {
                    files.push(appstream_path);
                }
            }
        }
    }

    files
}

fn appstream_remote_name(path: &PathBuf) -> Option<String> {
    let active = path.parent()?;
    let arch = active.parent()?;
    let remote = arch.parent()?;
    remote.file_name()?.to_str().map(|s| s.to_string())
}

fn appstream_installation(path: &PathBuf) -> Option<String> {
    let path = path.to_string_lossy();
    if path.contains("/var/lib/flatpak/appstream/") {
        Some("system".to_string())
    } else if path.contains("/flatpak/appstream/") {
        Some("user".to_string())
    } else {
        None
    }
}

/// Fallback to flatpak remote-ls command
fn fallback_to_remote_ls(keyword: Option<&str>) -> Result<()> {
    let output = Command::new("flatpak")
        .args(["remote-ls", "--app"])
        .output()?;

    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout);
        let keyword_lower = keyword.map(|s| s.to_lowercase());

        for line in stdout.lines() {
            let parts: Vec<&str> = line.split('\t').collect();
            let id = parts.first().copied().unwrap_or("");
            let name = parts.get(1).copied().unwrap_or("");
            let summary = parts.get(2).copied().unwrap_or("");

            if let Some(kw) = keyword_lower.as_ref() {
                let id_matches = id.to_lowercase().contains(kw);
                let name_matches = name.to_lowercase().contains(kw);
                let summary_matches = summary.to_lowercase().contains(kw);

                if !id_matches && !name_matches && !summary_matches {
                    continue;
                }
            }

            print_flatpak_row(id, name, summary, None, None)?;
        }
        Ok(())
    } else {
        Err(anyhow::anyhow!(
            "flatpak remote-ls failed: {}",
            String::from_utf8_lossy(&output.stderr)
        ))
    }
}

fn print_flatpak_row(
    id: &str,
    name: &str,
    summary: &str,
    remote: Option<&str>,
    installation: Option<&str>,
) -> Result<()> {
    let display = if summary.is_empty() {
        name.to_string()
    } else {
        format!("{name}\t{summary}")
    };

    let row = StreamingMenuItem::new(
        "flatpak",
        id,
        display,
        FlatpakSelectionPayload {
            app_id: id.to_string(),
            remote: remote.map(str::to_string),
            installation: installation.map(str::to_string),
        },
    )
    .preview(FzfPreview::Command(preview_command(PreviewId::Flatpak)))
    .preview_arg(flatpak_preview_arg(id, remote, installation))
    .encode()?;

    let mut out = io::BufWriter::new(io::stdout());
    writeln!(out, "{row}")?;
    out.flush()?;
    Ok(())
}

fn flatpak_preview_arg(app_id: &str, remote: Option<&str>, installation: Option<&str>) -> String {
    match (installation, remote) {
        (Some(installation), Some(remote)) if !installation.is_empty() && !remote.is_empty() => {
            format!("{installation}|{remote}|{app_id}")
        }
        (None, Some(remote)) if !remote.is_empty() => format!("{remote}|{app_id}"),
        _ => app_id.to_string(),
    }
}
