use crate::config::Config;
use glob::Pattern;
use log::{debug, warn};
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use std::ffi::OsStr;
use std::path::Path;
use unicode_segmentation::UnicodeSegmentation;
use walkdir::DirEntry;

// Regex-Patterns als statische Variablen für bessere Performance
static RE_INVALID: Lazy<Regex> = Lazy::new(|| Regex::new(r"[^\w.\-]").unwrap());
static RE_ADJACENT: Lazy<Regex> = Lazy::new(|| Regex::new(r"_\.|\._").unwrap());
static RE_MULTI: Lazy<Regex> = Lazy::new(|| Regex::new(r"[_\.]{2,}").unwrap());

/// Repräsentiert eine Transformations-Sequenz
#[derive(Debug, Clone)]
pub struct Sequence {
    pub name: &'static str,
    pub description: &'static str,
    pub apply_umlauts: bool,
    pub apply_case: CaseTransform,
    pub apply_emojis: bool,
    pub minimal_mode: bool,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CaseTransform {
    None,
    Lower,
    Upper,
}

impl Sequence {
    /// Gibt alle verfügbaren Sequences zurück
    pub fn all() -> Vec<Sequence> {
        vec![
            Sequence {
                name: "default",
                description: "Standard transformation: spaces→underscores, umlauts→ASCII, remove special chars",
                apply_umlauts: true,
                apply_case: CaseTransform::None,
                apply_emojis: true,
                minimal_mode: false,
            },
            Sequence {
                name: "lower",
                description: "Like default, but convert everything to lowercase",
                apply_umlauts: true,
                apply_case: CaseTransform::Lower,
                apply_emojis: true,
                minimal_mode: false,
            },
            Sequence {
                name: "upper",
                description: "Like default, but convert everything to UPPERCASE",
                apply_umlauts: true,
                apply_case: CaseTransform::Upper,
                apply_emojis: true,
                minimal_mode: false,
            },
            Sequence {
                name: "minimal",
                description: "Minimal changes: only replace spaces, keep umlauts and UTF-8",
                apply_umlauts: false,
                apply_case: CaseTransform::None,
                apply_emojis: false,
                minimal_mode: true,
            },
            Sequence {
                name: "utf-8",
                description: "UTF-8 friendly: spaces→underscores, keep umlauts, remove special chars",
                apply_umlauts: false,
                apply_case: CaseTransform::None,
                apply_emojis: true,
                minimal_mode: false,
            },
        ]
    }

    /// Findet eine Sequence nach Namen
    pub fn find(name: &str) -> Option<Sequence> {
        Self::all().into_iter().find(|s| s.name == name)
    }

    /// Gibt die Default-Sequence zurück
    pub fn default() -> Sequence {
        Self::find("default").unwrap()
    }
}

// Bekannte Doppel-Extensions (z.B. .tar.gz)
const DOUBLE_EXTENSIONS: &[&str] = &[
    ".tar.gz",
    ".tar.bz2",
    ".tar.xz",
    ".tar.zst",
    ".tar.lz",
    ".tar.Z",
];

/// Trennt Dateiname in Basis und Extension, berücksichtigt Doppel-Extensions
fn split_filename(filename: &str) -> (String, String) {
    // Prüfe auf bekannte Doppel-Extensions
    for double_ext in DOUBLE_EXTENSIONS {
        if filename.ends_with(double_ext) {
            let base_len = filename.len() - double_ext.len();
            if base_len > 0 {
                return (
                    filename[..base_len].to_string(),
                    double_ext.to_string(),
                );
            }
        }
    }

    // Standard-Fall: nur letzte Extension
    match filename.rsplit_once('.') {
        Some((b, e)) if !b.is_empty() => (b.to_string(), format!(".{e}")),
        _ => (filename.to_string(), String::new()),
    }
}

/// Bereinigt den übergebenen Dateinamen mit gegebener Sequence.
pub fn clean_filename(
    name: &OsStr,
    config: &Config,
    sequence: &Sequence,
    verbose: bool,
) -> Option<String> {
    let original = name.to_string_lossy();

    // Versteckte Dateien (mit führendem Punkt) korrekt behandeln
    let (hidden_prefix, rest) = match original.strip_prefix('.') {
        Some(rest) => (".", rest),
        None => ("", original.as_ref()),
    };

    // Stamm und Extension trennen (nur im Rest, nicht im hidden_prefix)
    let (mut base, mut ext) = split_filename(rest);

    // Platzhalter (C++, c++, C#, c#) anlegen
    base = preserve_special_identifiers(&base);
    ext = preserve_special_identifiers(&ext);

    // 1) Config-Replacements anwenden (immer zuerst)
    for (k, v) in &config.replacements {
        base = base.replace(k, v);
    }

    // 2) Sequence-basierte Umlaut-Ersetzung
    if sequence.apply_umlauts {
        base = apply_umlaut_replacements(&base);
    }

    // 3) Hardcoded replacements (Apostroph etc.)
    base = apply_hardcoded_replacements(&base);

    // 4) Case-Transformation (auf base UND extension anwenden)
    match sequence.apply_case {
        CaseTransform::Lower => {
            base = base.to_lowercase();
            ext = ext.to_lowercase();
        }
        CaseTransform::Upper => {
            base = base.to_uppercase();
            ext = ext.to_uppercase();
        }
        CaseTransform::None => {}
    }

    // 5) Emojis ersetzen (wenn aktiviert)
    if sequence.apply_emojis {
        base = replace_emojis_and_superscript(&base);
    }

    // 6) Ungültige Zeichen behandeln
    if sequence.minimal_mode {
        // Minimal: Nur Leerzeichen und gefährliche Zeichen
        base = base.replace(' ', "_");
        // Entferne nur absolut gefährliche Zeichen
        base = base
            .replace('/', "_")
            .replace('\\', "_")
            .replace('\0', "_")
            .replace('\n', "_");
    } else {
        // Standard: Alle ungültigen Zeichen → Unterstrich
        base = RE_INVALID.replace_all(&base, "_").to_string();
    }

    // Ungültige Kombinationen aus Punkt und Unterstrich
    base = RE_ADJACENT.replace_all(&base, ".").to_string();

    // Mehrfache Punkte/Unterstriche auf einen reduzieren
    base = RE_MULTI
        .replace_all(&base, |caps: &Captures| {
            if caps[0].contains('.') {
                "."
            } else {
                "_"
            }
        })
        .to_string();

    // Führender Punkt soll bleiben, führende Unterstriche sollen verschwinden
    base = trim_leading_underscores_preserve_leading_dot(&base);

    // Überflüssige Unterstriche und Punkte am Ende beseitigen
    base = base.trim_end_matches('_').to_string();
    base = base.trim_end_matches('.').to_string();

    // Falls komplett geleert, "unnamed"
    if base.is_empty() {
        base = "unnamed".to_string();
    }

    // Endgültigen Dateinamen zusammenbauen (hidden_prefix wieder hinzufügen)
    let mut result = format!("{}{}{}", hidden_prefix, base, ext);

    // Platzhalter zurückverwandeln
    result = restore_special_identifiers(&result);

    // Falls --verbose und sich der Name geändert hat
    if verbose && result != original {
        debug!("Transformiert: '{}' -> '{}'", original, result);
    }

    // Keine Änderung -> None zurückgeben
    if result == *original {
        None
    } else {
        Some(result)
    }
}

/// Schützt spezielle Identifikatoren vor der Umwandlung
fn preserve_special_identifiers(input: &str) -> String {
    input
        .replace("C++", "CPLUSPLUS")
        .replace("c++", "cplusplus")
        .replace("C#", "CSHARP")
        .replace("c#", "csharp")
}

/// Stellt spezielle Identifikatoren wieder her
fn restore_special_identifiers(input: &str) -> String {
    input
        .replace("CPLUSPLUS", "C++")
        .replace("cplusplus", "c++")
        .replace("CSHARP", "C#")
        .replace("csharp", "c#")
}

/// Fasst alle fest eingebauten Ersetzungen zusammen.
fn apply_hardcoded_replacements(input: &str) -> String {
    input
        .replace('\'', "") // Apostroph entfernen
        .replace("ˆ", "_")
}

/// Ersetzt deutsche Umlaute durch ASCII-Äquivalente
fn apply_umlaut_replacements(input: &str) -> String {
    input
        .replace("ä", "ae")
        .replace("ö", "oe")
        .replace("ü", "ue")
        .replace("Ä", "Ae")
        .replace("Ö", "Oe")
        .replace("Ü", "Ue")
        .replace("ß", "ss")
}

/// Entfernt am Anfang nur Unterstriche, einen führenden Punkt (.) bewahrt es.
fn trim_leading_underscores_preserve_leading_dot(s: &str) -> String {
    let mut chars = s.chars().peekable();
    let mut result = String::new();

    if let Some('.') = chars.peek() {
        // Nimm den Punkt
        result.push('.');
        chars.next();

        // Entferne anschließend führende Unterstriche hinter dem Punkt
        while let Some('_') = chars.peek() {
            chars.next();
        }
    } else {
        // Entferne führende Unterstriche
        while let Some('_') = chars.peek() {
            chars.next();
        }
    }

    // Restliche Zeichen anfügen
    result.extend(chars);
    result
}

/// Ersetzt Emojis und hochgestellte Zeichen (z. B. ²³⁴) durch '_'.
fn replace_emojis_and_superscript(input: &str) -> String {
    input
        .graphemes(true)
        .map(|g| {
            if emojis::get(g).is_some() || is_superscript(g) {
                "_".to_string()
            } else {
                g.to_string()
            }
        })
        .collect()
}

/// Prüft, ob alle Zeichen ein Superscript sind (z. B. ²³⁴).
fn is_superscript(g: &str) -> bool {
    g.chars().all(|c| {
        c == '\u{00AA}'
            || c == '\u{00BA}'
            || ('\u{00B2}'..='\u{00B3}').contains(&c)
            || c == '\u{00B9}'
            || ('\u{2070}'..='\u{209F}').contains(&c)
    })
}

/// Prüft, ob der Pfad aufgrund der Ausschlussmuster ignoriert werden soll.
pub fn is_excluded(entry: &DirEntry, patterns: &[Pattern]) -> bool {
    let path = entry.path();
    patterns.iter().any(|p| p.matches_path(path))
}

/// Überprüft, ob eine Umbenennungsoperation sicher ist
pub fn is_safe_rename(src: &Path, dst: &Path, force: bool) -> bool {
    // Prüfen ob Ziel bereits existiert
    if src.exists() && dst.exists() && !force {
        warn!(
            "Ziel existiert bereits und --force nicht gesetzt: {}",
            dst.display()
        );
        return false;
    }

    // Prüfen ob Quell-Datei überhaupt existiert
    if !src.exists() {
        warn!("Quell-Datei existiert nicht: {}", src.display());
        return false;
    }

    // Prüfen ob Parent-Verzeichnis der Quelle schreibbar ist
    if let Some(parent) = src.parent() {
        match std::fs::metadata(parent) {
            Ok(metadata) => {
                #[cfg(unix)]
                {
                    use std::os::unix::fs::PermissionsExt;
                    let permissions = metadata.permissions();
                    if permissions.mode() & 0o200 == 0 {
                        warn!(
                            "Keine Schreibrechte im Verzeichnis: {}",
                            parent.display()
                        );
                        return false;
                    }
                }
            }
            Err(e) => {
                warn!(
                    "Kann Metadaten des Verzeichnisses nicht lesen {}: {}",
                    parent.display(),
                    e
                );
                return false;
            }
        }
    }

    // Prüfen ob Parent-Verzeichnis des Ziels schreibbar ist (falls unterschiedlich)
    if let Some(dst_parent) = dst.parent() {
        if src.parent() != Some(dst_parent) {
            match std::fs::metadata(dst_parent) {
                Ok(metadata) => {
                    #[cfg(unix)]
                    {
                        use std::os::unix::fs::PermissionsExt;
                        let permissions = metadata.permissions();
                        if permissions.mode() & 0o200 == 0 {
                            warn!(
                                "Keine Schreibrechte im Ziel-Verzeichnis: {}",
                                dst_parent.display()
                            );
                            return false;
                        }
                    }
                }
                Err(e) => {
                    warn!(
                        "Kann Metadaten des Ziel-Verzeichnisses nicht lesen {}: {}",
                        dst_parent.display(),
                        e
                    );
                    return false;
                }
            }
        }
    }

    true
}

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

    fn make_test_config() -> Config {
        Config::default()
    }

    #[test]
    fn test_clean_filename_basic() {
        let config = Config::default();
        let sequence = Sequence::default();

        // Spaces should become underscores
        assert_eq!(
            clean_filename(OsStr::new("test file.txt"), &config, &sequence, false),
            Some("test_file.txt".to_string())
        );

        // Parentheses should become underscores
        assert_eq!(
            clean_filename(OsStr::new("file (1).txt"), &config, &sequence, false),
            Some("file_1.txt".to_string())
        );

        // Multiple underscores should be collapsed
        assert_eq!(
            clean_filename(OsStr::new("test__file.txt"), &config, &sequence, false),
            Some("test_file.txt".to_string())
        );
    }

    #[test]
    fn test_clean_filename_hidden_files() {
        let config = Config::default();
        let sequence = Sequence::default();

        // Hidden files should keep their leading dot
        assert_eq!(
            clean_filename(OsStr::new(".gitignore"), &config, &sequence, false),
            None // No change needed
        );

        // Hidden files with spaces
        assert_eq!(
            clean_filename(OsStr::new(".my config"), &config, &sequence, false),
            Some(".my_config".to_string())
        );

        // Hidden files with extension
        assert_eq!(
            clean_filename(OsStr::new(".test file.txt"), &config, &sequence, false),
            Some(".test_file.txt".to_string())
        );

        // Multiple leading dots
        assert_eq!(
            clean_filename(OsStr::new("...strange"), &config, &sequence, false),
            Some(".unnamed.strange".to_string())
        );
    }

    #[test]
    fn test_clean_filename_umlauts() {
        let config = make_test_config();
        let sequence = Sequence::default();

        // German umlauts
        assert_eq!(
            clean_filename(OsStr::new("Müller.pdf"), &config, &sequence, false),
            Some("Mueller.pdf".to_string())
        );

        assert_eq!(
            clean_filename(OsStr::new("schön.txt"), &config, &sequence, false),
            Some("schoen.txt".to_string())
        );

        assert_eq!(
            clean_filename(OsStr::new("Größe.doc"), &config, &sequence, false),
            Some("Groesse.doc".to_string())
        );
    }

    #[test]
    fn test_clean_filename_extensions() {
        let config = Config::default();
        let sequence = Sequence::default();

        // Single extension
        assert_eq!(
            clean_filename(OsStr::new("test file.txt"), &config, &sequence, false),
            Some("test_file.txt".to_string())
        );

        // Double extension with spaces in base name
        assert_eq!(
            clean_filename(OsStr::new("my archive.tar.gz"), &config, &sequence, false),
            Some("my_archive.tar.gz".to_string())
        );

        // Other double extensions
        assert_eq!(
            clean_filename(OsStr::new("backup file.tar.bz2"), &config, &sequence, false),
            Some("backup_file.tar.bz2".to_string())
        );

        assert_eq!(
            clean_filename(OsStr::new("data set.tar.xz"), &config, &sequence, false),
            Some("data_set.tar.xz".to_string())
        );

        // Multiple dots (not a double extension)
        assert_eq!(
            clean_filename(OsStr::new("foo..bar.txt"), &config, &sequence, false),
            Some("foo.bar.txt".to_string())
        );
    }

    #[test]
    fn test_split_filename() {
        // Double extensions
        assert_eq!(
            split_filename("archive.tar.gz"),
            ("archive".to_string(), ".tar.gz".to_string())
        );

        assert_eq!(
            split_filename("backup.tar.bz2"),
            ("backup".to_string(), ".tar.bz2".to_string())
        );

        // Single extension
        assert_eq!(
            split_filename("file.txt"),
            ("file".to_string(), ".txt".to_string())
        );

        // No extension
        assert_eq!(
            split_filename("README"),
            ("README".to_string(), String::new())
        );
    }

    #[test]
    fn test_clean_filename_special_identifiers() {
        let config = Config::default();
        let sequence = Sequence::default();

        // C++ should be preserved
        assert_eq!(
            clean_filename(OsStr::new("test C++.txt"), &config, &sequence, false),
            Some("test_C++.txt".to_string())
        );

        // C# should be preserved
        assert_eq!(
            clean_filename(OsStr::new("guide C#.pdf"), &config, &sequence, false),
            Some("guide_C#.pdf".to_string())
        );
    }

    #[test]
    fn test_clean_filename_no_change_needed() {
        let config = Config::default();
        let sequence = Sequence::default();

        // Already clean filenames should return None
        assert_eq!(
            clean_filename(OsStr::new("clean_file.txt"), &config, &sequence, false),
            None
        );

        assert_eq!(
            clean_filename(OsStr::new("another-file.pdf"), &config, &sequence, false),
            None
        );
    }

    #[test]
    fn test_clean_filename_empty_after_cleaning() {
        let config = Config::default();
        let sequence = Sequence::default();

        // File with only special chars should become "unnamed"
        assert_eq!(
            clean_filename(OsStr::new("###.txt"), &config, &sequence, false),
            Some("unnamed.txt".to_string())
        );
    }

    #[test]
    fn test_clean_filename_apostrophe() {
        let config = Config::default();
        let sequence = Sequence::default();

        // Apostrophes should be removed (not replaced with underscore)
        assert_eq!(
            clean_filename(OsStr::new("O'Reilly.pdf"), &config, &sequence, false),
            Some("OReilly.pdf".to_string())
        );
    }
}
