use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

use quick_xml::events::Event;
use quick_xml::Reader;

use super::database::Database;

// ---------------------------------------------------------------------------
// AppStream metainfo XML parser - reads metadata FROM AppImages
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Default)]
pub struct AppStreamMetadata {
    pub id: Option<String>,
    pub name: Option<String>,
    pub summary: Option<String>,
    pub description: Option<String>,
    pub developer: Option<String>,
    pub project_license: Option<String>,
    pub project_group: Option<String>,
    pub urls: HashMap<String, String>,
    pub keywords: Vec<String>,
    pub categories: Vec<String>,
    pub content_rating_summary: Option<String>,
    pub releases: Vec<ReleaseInfo>,
    pub mime_types: Vec<String>,
    pub screenshot_urls: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct ReleaseInfo {
    pub version: String,
    pub date: Option<String>,
    pub description: Option<String>,
}

/// Parse an AppStream metainfo XML file and extract all metadata.
pub fn parse_appstream_file(path: &Path) -> Option<AppStreamMetadata> {
    let content = std::fs::read_to_string(path).ok()?;
    parse_appstream_xml(&content)
}

/// Parse AppStream XML from a string.
pub fn parse_appstream_xml(xml: &str) -> Option<AppStreamMetadata> {
    let mut reader = Reader::from_str(xml);
    let mut meta = AppStreamMetadata::default();
    let mut buf = Vec::new();

    let mut current_tag = String::new();
    let mut in_component = false;
    let mut in_description = false;
    let mut in_release = false;
    let mut in_release_description = false;
    let mut in_provides = false;
    let mut in_keywords = false;
    let mut in_categories = false;
    let mut description_parts: Vec<String> = Vec::new();
    let mut release_desc_parts: Vec<String> = Vec::new();
    let mut current_url_type = String::new();
    let mut current_release_version = String::new();
    let mut current_release_date = String::new();
    let mut content_rating_attrs: Vec<(String, String)> = Vec::new();
    let mut in_content_rating = false;
    let mut current_content_attr_id = String::new();
    let mut in_developer = false;
    let mut in_screenshots = false;
    let mut in_screenshot_image = false;
    let mut depth = 0u32;
    let mut description_depth = 0u32;
    let mut release_desc_depth = 0u32;

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(ref e)) => {
                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
                depth += 1;

                match tag_name.as_str() {
                    "component" => {
                        in_component = true;
                    }
                    "description" if in_component && !in_release => {
                        in_description = true;
                        description_depth = depth;
                        description_parts.clear();
                    }
                    "description" if in_release => {
                        in_release_description = true;
                        release_desc_depth = depth;
                        release_desc_parts.clear();
                    }
                    "p" if in_description && !in_release_description => {
                        current_tag = "description_p".to_string();
                    }
                    "li" if in_description && !in_release_description => {
                        current_tag = "description_li".to_string();
                    }
                    "p" if in_release_description => {
                        current_tag = "release_desc_p".to_string();
                    }
                    "li" if in_release_description => {
                        current_tag = "release_desc_li".to_string();
                    }
                    "url" if in_component => {
                        current_url_type = String::new();
                        for attr in e.attributes().flatten() {
                            if attr.key.as_ref() == b"type" {
                                current_url_type =
                                    String::from_utf8_lossy(&attr.value).to_string();
                            }
                        }
                        current_tag = "url".to_string();
                    }
                    "release" if in_component => {
                        in_release = true;
                        current_release_version.clear();
                        current_release_date.clear();
                        release_desc_parts.clear();
                        for attr in e.attributes().flatten() {
                            match attr.key.as_ref() {
                                b"version" => {
                                    current_release_version =
                                        String::from_utf8_lossy(&attr.value).to_string();
                                }
                                b"date" => {
                                    current_release_date =
                                        String::from_utf8_lossy(&attr.value).to_string();
                                }
                                _ => {}
                            }
                        }
                    }
                    "content_rating" if in_component => {
                        in_content_rating = true;
                        content_rating_attrs.clear();
                    }
                    "content_attribute" if in_content_rating => {
                        current_content_attr_id.clear();
                        for attr in e.attributes().flatten() {
                            if attr.key.as_ref() == b"id" {
                                current_content_attr_id =
                                    String::from_utf8_lossy(&attr.value).to_string();
                            }
                        }
                        current_tag = "content_attribute".to_string();
                    }
                    "provides" if in_component => {
                        in_provides = true;
                    }
                    "mediatype" if in_provides => {
                        current_tag = "mediatype".to_string();
                    }
                    "keywords" if in_component => {
                        in_keywords = true;
                    }
                    "keyword" if in_keywords => {
                        current_tag = "keyword".to_string();
                    }
                    "categories" if in_component => {
                        in_categories = true;
                    }
                    "category" if in_categories => {
                        current_tag = "category".to_string();
                    }
                    "screenshots" if in_component => {
                        in_screenshots = true;
                    }
                    "image" if in_screenshots => {
                        // Prefer "source" type, but accept any <image>
                        in_screenshot_image = true;
                        current_tag = "screenshot_image".to_string();
                    }
                    "developer" if in_component => {
                        in_developer = true;
                    }
                    "developer_name" if in_component && !in_developer => {
                        current_tag = "developer_name".to_string();
                    }
                    "name" if in_developer => {
                        current_tag = "developer_child_name".to_string();
                    }
                    "id" if in_component && depth == 2 => {
                        current_tag = "id".to_string();
                    }
                    "name" if in_component && !in_developer && depth == 2 => {
                        let has_lang = e.attributes().flatten().any(|a| {
                            a.key.as_ref() == b"xml:lang" || a.key.as_ref() == b"lang"
                        });
                        if !has_lang {
                            current_tag = "name".to_string();
                        }
                    }
                    "summary" if in_component && depth == 2 => {
                        let has_lang = e.attributes().flatten().any(|a| {
                            a.key.as_ref() == b"xml:lang" || a.key.as_ref() == b"lang"
                        });
                        if !has_lang {
                            current_tag = "summary".to_string();
                        }
                    }
                    "project_license" if in_component => {
                        current_tag = "project_license".to_string();
                    }
                    "project_group" if in_component => {
                        current_tag = "project_group".to_string();
                    }
                    _ => {}
                }
            }
            Ok(Event::End(ref e)) => {
                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();

                match tag_name.as_str() {
                    "component" => {
                        in_component = false;
                    }
                    "description"
                        if in_release_description && depth == release_desc_depth =>
                    {
                        in_release_description = false;
                    }
                    "description" if in_description && depth == description_depth => {
                        in_description = false;
                        if !description_parts.is_empty() {
                            meta.description = Some(description_parts.join("\n\n"));
                        }
                    }
                    "release" => {
                        if !current_release_version.is_empty()
                            && meta.releases.len() < 10
                        {
                            let desc = if release_desc_parts.is_empty() {
                                None
                            } else {
                                Some(release_desc_parts.join("\n"))
                            };
                            meta.releases.push(ReleaseInfo {
                                version: current_release_version.clone(),
                                date: if current_release_date.is_empty() {
                                    None
                                } else {
                                    Some(current_release_date.clone())
                                },
                                description: desc,
                            });
                        }
                        in_release = false;
                        in_release_description = false;
                    }
                    "content_rating" => {
                        in_content_rating = false;
                        meta.content_rating_summary =
                            Some(summarize_content_rating(&content_rating_attrs));
                    }
                    "provides" => {
                        in_provides = false;
                    }
                    "keywords" => {
                        in_keywords = false;
                    }
                    "categories" => {
                        in_categories = false;
                    }
                    "screenshots" => {
                        in_screenshots = false;
                    }
                    "image" if in_screenshot_image => {
                        in_screenshot_image = false;
                    }
                    "developer" => {
                        in_developer = false;
                    }
                    _ => {}
                }

                current_tag.clear();
                depth = depth.saturating_sub(1);
            }
            Ok(Event::Text(ref e)) => {
                let text = e.unescape().unwrap_or_default().trim().to_string();
                if text.is_empty() {
                    continue;
                }

                match current_tag.as_str() {
                    "id" => meta.id = Some(text),
                    "name" => meta.name = Some(text),
                    "summary" => meta.summary = Some(text),
                    "project_license" => meta.project_license = Some(text),
                    "project_group" => meta.project_group = Some(text),
                    "url" if !current_url_type.is_empty() => {
                        meta.urls.insert(current_url_type.clone(), text);
                    }
                    "description_p" => {
                        description_parts.push(text);
                    }
                    "description_li" => {
                        description_parts.push(format!("  - {}", text));
                    }
                    "release_desc_p" => {
                        release_desc_parts.push(text);
                    }
                    "release_desc_li" => {
                        release_desc_parts.push(format!("  - {}", text));
                    }
                    "content_attribute" if !current_content_attr_id.is_empty() => {
                        content_rating_attrs
                            .push((current_content_attr_id.clone(), text));
                    }
                    "mediatype" => {
                        meta.mime_types.push(text);
                    }
                    "keyword" => {
                        meta.keywords.push(text);
                    }
                    "category" => {
                        meta.categories.push(text);
                    }
                    "screenshot_image" => {
                        if text.starts_with("http") && meta.screenshot_urls.len() < 10 {
                            meta.screenshot_urls.push(text);
                        }
                    }
                    "developer_name" => {
                        if meta.developer.is_none() {
                            meta.developer = Some(text);
                        }
                    }
                    "developer_child_name" => {
                        meta.developer = Some(text);
                    }
                    _ => {}
                }
            }
            Ok(Event::Empty(ref e)) => {
                let tag_name =
                    String::from_utf8_lossy(e.name().as_ref()).to_string();
                if tag_name == "release" && in_component {
                    let mut ver = String::new();
                    let mut date = String::new();
                    for attr in e.attributes().flatten() {
                        match attr.key.as_ref() {
                            b"version" => {
                                ver = String::from_utf8_lossy(&attr.value)
                                    .to_string();
                            }
                            b"date" => {
                                date = String::from_utf8_lossy(&attr.value)
                                    .to_string();
                            }
                            _ => {}
                        }
                    }
                    if !ver.is_empty() && meta.releases.len() < 10 {
                        meta.releases.push(ReleaseInfo {
                            version: ver,
                            date: if date.is_empty() { None } else { Some(date) },
                            description: None,
                        });
                    }
                }
            }
            Ok(Event::Eof) => break,
            Err(e) => {
                log::warn!("AppStream XML parse error: {}", e);
                break;
            }
            _ => {}
        }
        buf.clear();
    }

    if meta.id.is_some() || meta.name.is_some() || meta.summary.is_some() {
        Some(meta)
    } else {
        None
    }
}

/// Summarize OARS content rating attributes into a human-readable level.
fn summarize_content_rating(attrs: &[(String, String)]) -> String {
    let max_level = attrs
        .iter()
        .map(|(_, v)| match v.as_str() {
            "intense" => 3,
            "moderate" => 2,
            "mild" => 1,
            _ => 0,
        })
        .max()
        .unwrap_or(0);

    match max_level {
        0 => "All ages".to_string(),
        1 => "Mild content".to_string(),
        2 => "Moderate content".to_string(),
        3 => "Mature content".to_string(),
        _ => "Unknown".to_string(),
    }
}

// ---------------------------------------------------------------------------
// AppStream catalog generation - writes catalog XML for GNOME Software/Discover
// ---------------------------------------------------------------------------

/// Generate an AppStream catalog XML from the Driftwood database.
/// This allows GNOME Software / KDE Discover to see locally managed AppImages.
pub fn generate_catalog(db: &Database) -> Result<String, AppStreamError> {
    let records = db.get_all_appimages()
        .map_err(|e| AppStreamError::Database(e.to_string()))?;

    let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
    xml.push_str("<components version=\"0.16\" origin=\"driftwood\">\n");

    for record in &records {
        let app_name = record.app_name.as_deref().unwrap_or(&record.filename);
        let app_id = make_component_id(app_name);
        let description = record.description.as_deref().unwrap_or("");

        xml.push_str("  <component type=\"desktop-application\">\n");
        xml.push_str(&format!("    <id>appimage.{}</id>\n", xml_escape(&app_id)));
        xml.push_str(&format!("    <name>{}</name>\n", xml_escape(app_name)));

        if !description.is_empty() {
            xml.push_str(&format!("    <summary>{}</summary>\n", xml_escape(description)));
        }

        xml.push_str(&format!("    <pkgname>{}</pkgname>\n", xml_escape(&record.filename)));

        if let Some(version) = &record.app_version {
            xml.push_str("    <releases>\n");
            xml.push_str(&format!(
                "      <release version=\"{}\" />\n",
                xml_escape(version),
            ));
            xml.push_str("    </releases>\n");
        }

        if let Some(categories) = &record.categories {
            xml.push_str("    <categories>\n");
            for cat in categories.split(';').filter(|c| !c.is_empty()) {
                xml.push_str(&format!("      <category>{}</category>\n", xml_escape(cat.trim())));
            }
            xml.push_str("    </categories>\n");
        }

        // Provide hint about source (AppStream spec uses <custom> for key-value data)
        xml.push_str("    <custom>\n");
        xml.push_str("      <value key=\"Driftwood::managed-by\">driftwood</value>\n");
        xml.push_str(&format!(
            "      <value key=\"Driftwood::appimage-path\">{}</value>\n",
            xml_escape(&record.path),
        ));
        xml.push_str("    </custom>\n");

        xml.push_str("  </component>\n");
    }

    xml.push_str("</components>\n");
    Ok(xml)
}

/// Install the AppStream catalog to the local swcatalog directory.
/// GNOME Software reads from `~/.local/share/swcatalog/xml/`.
pub fn install_catalog(db: &Database) -> Result<PathBuf, AppStreamError> {
    let catalog_xml = generate_catalog(db)?;

    let catalog_dir = crate::config::data_dir_fallback()
        .join("swcatalog")
        .join("xml");

    fs::create_dir_all(&catalog_dir)
        .map_err(|e| AppStreamError::Io(e.to_string()))?;

    let catalog_path = catalog_dir.join("driftwood.xml");
    fs::write(&catalog_path, &catalog_xml)
        .map_err(|e| AppStreamError::Io(e.to_string()))?;

    Ok(catalog_path)
}

/// Remove the AppStream catalog from the local swcatalog directory.
pub fn uninstall_catalog() -> Result<(), AppStreamError> {
    let catalog_path = crate::config::data_dir_fallback()
        .join("swcatalog")
        .join("xml")
        .join("driftwood.xml");

    if catalog_path.exists() {
        fs::remove_file(&catalog_path)
            .map_err(|e| AppStreamError::Io(e.to_string()))?;
    }

    Ok(())
}

/// Check if the AppStream catalog is currently installed.
pub fn is_catalog_installed() -> bool {
    let catalog_path = crate::config::data_dir_fallback()
        .join("swcatalog")
        .join("xml")
        .join("driftwood.xml");

    catalog_path.exists()
}

// --- Utility functions ---

fn make_component_id(name: &str) -> String {
    name.chars()
        .map(|c| if c.is_alphanumeric() || c == '-' || c == '.' { c.to_ascii_lowercase() } else { '_' })
        .collect::<String>()
        .trim_matches('_')
        .to_string()
}

fn xml_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&apos;")
}

// --- Error types ---

#[derive(Debug)]
pub enum AppStreamError {
    Database(String),
    Io(String),
}

impl std::fmt::Display for AppStreamError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Database(e) => write!(f, "Database error: {}", e),
            Self::Io(e) => write!(f, "I/O error: {}", e),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_make_component_id() {
        assert_eq!(make_component_id("Firefox"), "firefox");
        assert_eq!(make_component_id("My App 2.0"), "my_app_2.0");
        assert_eq!(make_component_id("GIMP"), "gimp");
    }

    #[test]
    fn test_xml_escape() {
        assert_eq!(xml_escape("hello & world"), "hello &amp; world");
        assert_eq!(xml_escape("<tag>"), "&lt;tag&gt;");
        assert_eq!(xml_escape("it's \"quoted\""), "it&apos;s &quot;quoted&quot;");
    }

    #[test]
    fn test_generate_catalog_empty() {
        let db = crate::core::database::Database::open_in_memory().unwrap();
        let xml = generate_catalog(&db).unwrap();
        assert!(xml.contains("<components"));
        assert!(xml.contains("</components>"));
        // No individual component entries in an empty DB
        assert!(!xml.contains("<component "));
    }

    #[test]
    fn test_generate_catalog_with_app() {
        let db = crate::core::database::Database::open_in_memory().unwrap();
        db.upsert_appimage(
            "/tmp/test.AppImage",
            "test.AppImage",
            Some(2),
            1024,
            true,
            None,
        ).unwrap();
        db.update_metadata(
            1,
            Some("TestApp"),
            Some("1.0"),
            None,
            None,
            Some("Utility;"),
            None,
            None,
            None,
        ).ok();

        let xml = generate_catalog(&db).unwrap();
        assert!(xml.contains("appimage.testapp"));
        assert!(xml.contains("<pkgname>test.AppImage</pkgname>"));
        assert!(xml.contains("managed-by"));
    }

    #[test]
    fn test_appstream_error_display() {
        let err = AppStreamError::Database("db error".to_string());
        assert!(format!("{}", err).contains("db error"));
        let err = AppStreamError::Io("write failed".to_string());
        assert!(format!("{}", err).contains("write failed"));
    }

    #[test]
    fn test_parse_minimal_appstream() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
  <id>org.example.TestApp</id>
  <name>Test App</name>
  <summary>A test application</summary>
  <metadata_license>CC0-1.0</metadata_license>
  <project_license>MIT</project_license>
</component>"#;
        let meta = parse_appstream_xml(xml).unwrap();
        assert_eq!(meta.id.as_deref(), Some("org.example.TestApp"));
        assert_eq!(meta.name.as_deref(), Some("Test App"));
        assert_eq!(meta.summary.as_deref(), Some("A test application"));
        assert_eq!(meta.project_license.as_deref(), Some("MIT"));
    }

    #[test]
    fn test_parse_full_appstream() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
  <id>org.kde.krita</id>
  <name>Krita</name>
  <name xml:lang="de">Krita</name>
  <summary>Digital Painting, Creative Freedom</summary>
  <developer>
    <name>KDE Community</name>
  </developer>
  <project_license>GPL-3.0-or-later</project_license>
  <project_group>KDE</project_group>
  <description>
    <p>Krita is a creative painting application.</p>
    <p>Features include:</p>
    <ul>
      <li>Layer support</li>
      <li>Brush engines</li>
    </ul>
  </description>
  <url type="homepage">https://krita.org</url>
  <url type="bugtracker">https://bugs.kde.org</url>
  <url type="donation">https://krita.org/support-us/donations</url>
  <keywords>
    <keyword>paint</keyword>
    <keyword>draw</keyword>
  </keywords>
  <content_rating type="oars-1.1">
    <content_attribute id="violence-cartoon">none</content_attribute>
    <content_attribute id="language-humor">mild</content_attribute>
  </content_rating>
  <releases>
    <release version="5.2.0" date="2024-01-15">
      <description>
        <p>Major update with new features.</p>
      </description>
    </release>
    <release version="5.1.0" date="2023-09-01"/>
  </releases>
  <provides>
    <mediatype>image/png</mediatype>
    <mediatype>image/jpeg</mediatype>
  </provides>
</component>"#;
        let meta = parse_appstream_xml(xml).unwrap();
        assert_eq!(meta.id.as_deref(), Some("org.kde.krita"));
        assert_eq!(meta.name.as_deref(), Some("Krita"));
        assert_eq!(meta.developer.as_deref(), Some("KDE Community"));
        assert_eq!(meta.project_license.as_deref(), Some("GPL-3.0-or-later"));
        assert_eq!(meta.project_group.as_deref(), Some("KDE"));
        assert_eq!(
            meta.urls.get("homepage").map(|s| s.as_str()),
            Some("https://krita.org")
        );
        assert_eq!(
            meta.urls.get("bugtracker").map(|s| s.as_str()),
            Some("https://bugs.kde.org")
        );
        assert_eq!(meta.keywords, vec!["paint", "draw"]);
        assert_eq!(
            meta.content_rating_summary.as_deref(),
            Some("Mild content")
        );
        assert_eq!(meta.releases.len(), 2);
        assert_eq!(meta.releases[0].version, "5.2.0");
        assert_eq!(meta.releases[0].date.as_deref(), Some("2024-01-15"));
        assert!(meta.releases[0].description.is_some());
        assert_eq!(meta.releases[1].version, "5.1.0");
        assert_eq!(meta.mime_types, vec!["image/png", "image/jpeg"]);
        assert!(meta
            .description
            .unwrap()
            .contains("Krita is a creative painting application"));
    }

    #[test]
    fn test_parse_legacy_developer_name() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
  <id>org.test.App</id>
  <name>Test</name>
  <summary>Test app</summary>
  <developer_name>Old Developer Tag</developer_name>
</component>"#;
        let meta = parse_appstream_xml(xml).unwrap();
        assert_eq!(meta.developer.as_deref(), Some("Old Developer Tag"));
    }

    #[test]
    fn test_parse_empty_returns_none() {
        let xml = r#"<?xml version="1.0"?><component></component>"#;
        assert!(parse_appstream_xml(xml).is_none());
    }

    #[test]
    fn test_content_rating_levels() {
        assert_eq!(summarize_content_rating(&[]), "All ages");
        assert_eq!(
            summarize_content_rating(&[
                ("violence-cartoon".to_string(), "none".to_string()),
                ("language-humor".to_string(), "mild".to_string()),
            ]),
            "Mild content"
        );
        assert_eq!(
            summarize_content_rating(&[(
                "violence-realistic".to_string(),
                "intense".to_string(),
            )]),
            "Mature content"
        );
    }
}
