use anyhow::{Context, Result, anyhow};
use clap::Parser;
use std::{
    collections::BTreeMap,
    fmt::Write as W2,
    fs::{File, read_to_string},
    io::Write,
    path::{Path, PathBuf},
};

#[derive(Parser)]
enum Args {
    ImportOldPot {
        input: PathBuf,
        output: PathBuf,
    },
    ImportOldPo {
        reference: PathBuf,
        input: PathBuf,
        output: PathBuf,
    },
    ExportJson {
        output: PathBuf,
        inputs: Vec<PathBuf>,
    },
    ExportGodotCsv {
        input_dir: PathBuf,
        output: PathBuf,
    },
    ExportPo {
        #[arg(long)]
        remap_ids: Option<PathBuf>,
        output: PathBuf,
        inputs: Vec<PathBuf>,
    },
}

static NATIVE_LANGUAGE_NAMES: &[(&str, &str)] = &[
    ("en", "English"),
    ("de", "Deutsch"),
    ("fr", "Français"),
    ("nl", "Nederlands"),
    ("es", "Español"),
    ("eu", "Euskara"),
    ("ja", "日本語"),
    ("he", "עִברִית"),
    ("tr", "Türkçe"),
    ("fi", "Suomi"),
    ("ar", "العربية"),
    ("zh_Hans", "中文 (简化字)"),
    ("zh_Hant", "中文 (繁體字)"),
    ("pl", "Polski"),
    ("pt", "Português"),
    ("it", "Italiano"),
    ("ko", "한국인"),
    ("el", "ελληνικά"),
    ("ru", "русский"),
];

fn export_load(inputs: &[PathBuf]) -> Result<BTreeMap<String, String>> {
    let mut ini = BTreeMap::new();
    for path in inputs {
        let f = load_ini(path)?;
        for (k, v) in f {
            ini.entry(k).or_insert(v);
        }
    }
    for &(code, name) in NATIVE_LANGUAGE_NAMES {
        ini.insert(format!("c.settings.ui.language.{code}"), name.to_owned());
    }
    Ok(ini)
}

fn main() -> Result<()> {
    let args = Args::parse();
    match args {
        Args::ExportJson { inputs, output } => {
            let ini = export_load(&inputs)?;
            File::create(output)?.write_all(serde_json::to_string(&ini)?.as_bytes())?;
            Ok(())
        }
        Args::ExportPo {
            remap_ids: id_map,
            output,
            inputs,
        } => {
            let ini = export_load(&inputs)?;
            let id_map = id_map.map(|path| load_ini(&path)).transpose()?;
            File::create(output)?.write_all(
                format!(
                    r#"
msgid ""
msgstr ""
"Language: {}\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

{}"#,
                    inputs
                        .first()
                        .ok_or(anyhow!("at least one input required"))?
                        .file_stem()
                        .ok_or(anyhow!("file name empty"))?
                        .to_string_lossy(),
                    ini.into_iter()
                        .try_fold(String::new(), |mut a, (mut key, value)| {
                            if let Some(id_map) = &id_map
                                && let Some(new_id) = id_map.get(&key)
                            {
                                key = new_id.to_owned()
                            }
                            let key = serde_json::to_string(&key)?;
                            let value = serde_json::to_string(&value)?;
                            writeln!(a, "msgid {key}\nmsgstr {value}\n\n",)?;
                            Ok::<_, anyhow::Error>(a)
                        })?
                )
                .as_bytes(),
            )?;
            Ok(())
        }
        Args::ExportGodotCsv { input_dir, output } => {
            let translations = input_dir
                .read_dir()?
                .flat_map(|e| {
                    e.map_err(|e| anyhow!("{e}"))
                        .and_then(|e| {
                            if e.file_name().to_string_lossy().ends_with(".ini") {
                                Ok(Some((
                                    e.path()
                                        .file_stem()
                                        .ok_or(anyhow!("empty filename"))?
                                        .to_string_lossy()
                                        .to_string(),
                                    load_ini(&e.path())?,
                                )))
                            } else {
                                Ok(None)
                            }
                        })
                        .transpose()
                })
                .collect::<Result<BTreeMap<_, _>>>()?;

            let langs = translations.keys().cloned().collect::<Vec<String>>();
            let mut tr_tr = BTreeMap::<String, BTreeMap<String, String>>::new();
            for (k, v) in translations {
                for (kk, vv) in v {
                    tr_tr.entry(kk).or_default().insert(k.clone(), vv);
                }
            }

            File::create(output)?.write_all(
                tr_tr
                    .into_iter()
                    .try_fold(format!("id,{}\n", langs.join(",")), |mut a, (k, v)| {
                        writeln!(
                            a,
                            "{k},{}",
                            v.values()
                                .map(serde_json::to_string)
                                .collect::<Result<Vec<_>, serde_json::Error>>()?
                                .join(",")
                        )?;
                        Ok::<_, anyhow::Error>(a)
                    })?
                    .as_bytes(),
            )?;
            Ok(())
        }
        Args::ImportOldPo {
            reference,
            input,
            output,
        } => {
            let reference = read_to_string(reference)?;
            let input = read_to_string(input)?;

            let id_reverse = reference
                .lines()
                .skip(1)
                .map(|l| {
                    l.split_once("=")
                        .map(|(k, v)| (v, k))
                        .ok_or(anyhow!("invalid ini"))
                })
                .collect::<Result<BTreeMap<&str, &str>>>()?;

            let mut outmap = BTreeMap::new();
            let mut mode = 0;
            let mut msgid = String::new();
            let mut msgstr = String::new();
            for (i, mut line) in input.lines().enumerate() {
                if line.starts_with("#") {
                    continue;
                }
                if line.is_empty() {
                    continue;
                }
                if let Some(rest) = line.strip_prefix("msgid ") {
                    if !msgid.is_empty() {
                        if let Some(id) = id_reverse.get(&msgid.as_str()) {
                            outmap.insert(id.to_owned(), msgstr.clone());
                        } else {
                            eprintln!("warning: message id {msgid:?} is unknown")
                        }
                    }
                    line = rest;
                    msgid = String::new();
                    mode = 1;
                } else if let Some(rest) = line.strip_prefix("msgstr ") {
                    line = rest;
                    msgstr = String::new();
                    mode = 2;
                } else if line.starts_with("msgctxt ") {
                    mode = 0;
                    eprintln!("warning: msgctxt not implemented (line {})", i + 1);
                    continue;
                }
                let frag =
                    serde_json::from_str::<String>(line).context(anyhow!("line {}", i + 1))?;
                match mode {
                    0 => (),
                    1 => msgid.push_str(&frag),
                    2 => msgstr.push_str(&frag),
                    _ => unreachable!(),
                };
            }

            File::create(output)?.write_all(
                outmap
                    .into_iter()
                    .try_fold("[hurrycurry]\n".to_string(), |mut a, (k, v)| {
                        writeln!(a, "{k}={v}")?;
                        Ok::<_, anyhow::Error>(a)
                    })?
                    .as_bytes(),
            )?;

            Ok(())
        }
        Args::ImportOldPot { input, output } => {
            let output_raw = read_to_string(&output).unwrap_or("".to_owned());
            let input = read_to_string(input)?;

            let mut output_flip = output_raw
                .lines()
                .skip(1)
                .map(|l| {
                    l.split_once("=")
                        .map(|(k, v)| (v.to_owned(), k.to_owned()))
                        .ok_or(anyhow!("invalid ini"))
                })
                .collect::<Result<BTreeMap<String, String>>>()?;

            let mut id = false;
            let mut msgid = String::new();
            for (i, mut line) in input.lines().enumerate() {
                if line.starts_with("#") {
                    continue;
                }
                if line.is_empty() {
                    continue;
                }
                if let Some(rest) = line.strip_prefix("msgid ") {
                    if !msgid.is_empty() && !output_flip.contains_key(&msgid) {
                        output_flip.insert(msgid.replace("\n", "\\n"), format!("unknown{i}"));
                    }
                    line = rest;
                    id = true;
                    msgid = String::new();
                } else if line.starts_with("msgctxt ") || line.starts_with("msgstr ") {
                    id = false;
                    continue;
                }
                if id {
                    let frag =
                        serde_json::from_str::<String>(line).context(anyhow!("line {}", i + 1))?;
                    msgid.push_str(frag.as_str());
                }
            }

            let output_unflip = output_flip
                .into_iter()
                .map(|(v, k)| (k, v))
                .collect::<BTreeMap<_, _>>();

            File::create(output)?.write_all(
                output_unflip
                    .into_iter()
                    .try_fold("[hurrycurry]\n".to_string(), |mut a, (k, v)| {
                        writeln!(a, "{k}={v}")?;
                        Ok::<_, anyhow::Error>(a)
                    })?
                    .as_bytes(),
            )?;

            Ok(())
        }
    }
}

fn load_ini(path: &Path) -> Result<BTreeMap<String, String>> {
    read_to_string(path)?
        .lines()
        .skip(1)
        .map(|l| {
            let (k, v) = l.split_once("=").ok_or(anyhow!("'=' missing"))?;
            Ok::<_, anyhow::Error>((k.trim_end().to_owned(), v.trim_start().replace("%n", "\n")))
        })
        .collect()
}
