use crate::{
    cli::{
        FileArgsCompat, PasswordArgs, SolidEntriesTransformStrategy,
        SolidEntriesTransformStrategyArgs,
    },
    command::{
        Command, ask_password,
        core::{
            SplitArchiveReader, TransformStrategyKeepSolid, TransformStrategyUnSolid,
            collect_split_archives,
        },
    },
    utils::{GlobPatterns, PathPartExt, env::NamedTempFile},
};
use base64::Engine;
use bstr::{ByteSlice, io::BufReadExt};
use clap::{ArgGroup, Parser, ValueEnum, ValueHint};
use indexmap::IndexMap;
use pna::NormalEntry;
use regex::Regex;
use std::{
    collections::HashMap,
    fmt::{self, Display, Formatter, Write},
    fs, io,
    str::FromStr,
};

#[derive(Parser, Clone, Eq, PartialEq, Hash, Debug)]
#[command(args_conflicts_with_subcommands = true, arg_required_else_help = true)]
pub(crate) struct XattrCommand {
    #[command(subcommand)]
    command: XattrCommands,
}

impl Command for XattrCommand {
    #[inline]
    fn execute(self, ctx: &crate::cli::GlobalContext) -> anyhow::Result<()> {
        match self.command {
            XattrCommands::Get(cmd) => cmd.execute(ctx),
            XattrCommands::Set(cmd) => cmd.execute(ctx),
        }
    }
}

#[derive(Parser, Clone, Eq, PartialEq, Hash, Debug)]
pub(crate) enum XattrCommands {
    #[command(about = "Get extended attributes of entries")]
    Get(GetXattrCommand),
    #[command(about = "Set extended attributes of entries")]
    Set(SetXattrCommand),
}

#[derive(Parser, Clone, Eq, PartialEq, Hash, Debug)]
#[command(
    group(ArgGroup::new("dump-flags").args(["name", "dump"])),
)]
pub(crate) struct GetXattrCommand {
    #[command(flatten)]
    file: FileArgsCompat,
    #[arg(short, long, help = "Dump the value of the named extended attribute")]
    name: Option<String>,
    #[arg(
        short,
        long,
        help = "Dump the values of all matched extended attributes"
    )]
    dump: bool,
    #[arg(
        short = 'm',
        long = "match",
        value_name = "pattern",
        help = "Only include attributes with names matching the regular expression pattern. Specify '-' for including all attributes"
    )]
    regex_match: Option<String>,
    #[arg(short, long, help = "Encode values after retrieving them")]
    encoding: Option<Encoding>,
    #[command(flatten)]
    password: PasswordArgs,
}

impl Command for GetXattrCommand {
    #[inline]
    fn execute(self, _ctx: &crate::cli::GlobalContext) -> anyhow::Result<()> {
        archive_get_xattr(self)
    }
}

#[derive(Parser, Clone, Eq, PartialEq, Hash, Debug)]
pub(crate) struct SetXattrCommand {
    #[command(flatten)]
    file: FileArgsCompat,
    #[arg(short, long, help = "Name of extended attribute")]
    name: Option<String>,
    #[arg(short, long, help = "Value of extended attribute")]
    value: Option<Value>,
    #[arg(
        short = 'x',
        long,
        value_name = "NAME",
        help = "Remove extended attribute"
    )]
    remove: Option<String>,
    #[arg(
        long,
        value_name = "FILE",
        help = "Restores extended attributes from file. The file must be in the format generated by the pna xattr get command with the --dump option. If a dash (-) is given as the file name, reads from standard input",
        value_hint = ValueHint::FilePath
    )]
    restore: Option<String>,
    #[command(flatten)]
    transform_strategy: SolidEntriesTransformStrategyArgs,
    #[command(flatten)]
    password: PasswordArgs,
}

impl Command for SetXattrCommand {
    #[inline]
    fn execute(self, _ctx: &crate::cli::GlobalContext) -> anyhow::Result<()> {
        archive_set_xattr(self)
    }
}

#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default, ValueEnum)]
#[value(rename_all = "lower")]
enum Encoding {
    #[default]
    Text,
    Hex,
    Base64,
}

impl Display for Encoding {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Encoding::Text => "text",
            Encoding::Hex => "hex",
            Encoding::Base64 => "base64",
        })
    }
}

enum MatchStrategy<'s> {
    All,
    Named(&'s str),
    Regex(Regex),
}

struct DumpOption<'s> {
    dump: bool,
    matcher: MatchStrategy<'s>,
}

impl<'a> DumpOption<'a> {
    #[inline]
    fn new(
        dump: bool,
        name: Option<&'a str>,
        regex_match: Option<&'a str>,
    ) -> Result<Self, regex::Error> {
        Ok(match (name, regex_match) {
            (None, None) | (None, Some("-")) => Self {
                dump,
                matcher: MatchStrategy::All,
            },
            (None, Some(re)) => Self {
                dump,
                matcher: MatchStrategy::Regex(Regex::new(re)?),
            },
            (Some(name), _) => Self {
                dump: true,
                matcher: MatchStrategy::Named(name),
            },
        })
    }

    #[inline]
    fn is_match(&self, name: &str) -> bool {
        match self.matcher {
            MatchStrategy::All => true,
            MatchStrategy::Named(n) => n == name,
            MatchStrategy::Regex(ref re) => re.is_match(name),
        }
    }
}

#[hooq::hooq(anyhow)]
fn archive_get_xattr(args: GetXattrCommand) -> anyhow::Result<()> {
    let password = ask_password(args.password)?;
    if args.file.files().is_empty() {
        return Ok(());
    }
    let files = args.file.files();
    let mut globs = GlobPatterns::new(files.iter().map(|p| p.as_str()))?;
    let encoding = args.encoding;
    let dump_option = DumpOption::new(args.dump, args.name.as_deref(), args.regex_match.as_deref())
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;

    let mut source = SplitArchiveReader::new(collect_split_archives(args.file.archive())?)?;

    source.for_each_entry(
        password.as_deref(),
        #[hooq::skip_all]
        |entry| {
            let entry = entry?;
            let name = entry.name();
            if globs.matches_any(name) {
                println!("# file: {name}");
                for attr in entry
                    .xattrs()
                    .iter()
                    .filter(|a| dump_option.is_match(a.name()))
                {
                    if dump_option.dump {
                        println!(
                            "{}={}",
                            attr.name(),
                            DisplayValue::new(attr.value(), encoding)
                        );
                    } else {
                        println!("{}", attr.name());
                    }
                }
                println!();
            }
            Ok(())
        },
    )?;
    globs.ensure_all_matched()?;
    Ok(())
}

#[hooq::hooq(anyhow)]
fn archive_set_xattr(args: SetXattrCommand) -> anyhow::Result<()> {
    let password = ask_password(args.password)?;
    let files = args.file.files();
    let mut set_strategy = if let Some("-") = args.restore.as_deref() {
        SetAttrStrategy::Restore(parse_dump(io::stdin().lock())?)
    } else if let Some(path) = args.restore.as_deref() {
        SetAttrStrategy::Restore(parse_dump(io::BufReader::new(fs::File::open(path)?))?)
    } else if files.is_empty() {
        return Ok(());
    } else {
        let globs = GlobPatterns::new(files.iter().map(|p| p.as_str()))?;
        let value = args.value.unwrap_or_default();
        SetAttrStrategy::Apply {
            globs,
            name: args.name,
            value,
            remove: args.remove,
        }
    };

    let mut source = SplitArchiveReader::new(collect_split_archives(args.file.archive())?)?;

    let output_path = args.file.archive().remove_part();
    let mut temp_file =
        NamedTempFile::new(|| output_path.parent().unwrap_or_else(|| ".".as_ref()))?;

    match args.transform_strategy.strategy() {
        SolidEntriesTransformStrategy::UnSolid => source.transform_entries(
            temp_file.as_file_mut(),
            password.as_deref(),
            #[hooq::skip_all]
            |entry| Ok(Some(set_strategy.transform_entry(entry?))),
            TransformStrategyUnSolid,
        ),
        SolidEntriesTransformStrategy::KeepSolid => source.transform_entries(
            temp_file.as_file_mut(),
            password.as_deref(),
            #[hooq::skip_all]
            |entry| Ok(Some(set_strategy.transform_entry(entry?))),
            TransformStrategyKeepSolid,
        ),
    }?;

    drop(source);

    temp_file.persist(output_path)?;

    if let SetAttrStrategy::Apply { globs, .. } = set_strategy {
        globs.ensure_all_matched()?;
    }
    Ok(())
}

enum SetAttrStrategy<'s> {
    Restore(HashMap<String, Vec<(String, Value)>>),
    Apply {
        globs: GlobPatterns<'s>,
        name: Option<String>,
        value: Value,
        remove: Option<String>,
    },
}

impl SetAttrStrategy<'_> {
    #[inline]
    fn transform_entry<T>(&mut self, entry: NormalEntry<T>) -> NormalEntry<T> {
        match self {
            SetAttrStrategy::Restore(restore) => {
                if let Some(attrs) = restore.get(entry.name().as_str()) {
                    let xattrs = entry
                        .xattrs()
                        .iter()
                        .map(|it| (it.name(), it.value()))
                        .chain(attrs.iter().map(|(k, v)| (k.as_str(), v.as_bytes())))
                        .collect::<IndexMap<_, _>>();
                    let xattrs = xattrs
                        .into_iter()
                        .map(|(key, value)| pna::ExtendedAttribute::new(key.into(), value.into()))
                        .collect::<Vec<_>>();
                    entry.with_xattrs(xattrs)
                } else {
                    entry
                }
            }
            SetAttrStrategy::Apply {
                globs,
                name,
                value,
                remove,
            } => {
                if globs.matches_any(entry.name()) {
                    transform_entry(entry, name.as_deref(), value.as_bytes(), remove.as_deref())
                } else {
                    entry
                }
            }
        }
    }
}

fn parse_dump(reader: impl io::BufRead) -> io::Result<HashMap<String, Vec<(String, Value)>>> {
    let mut result = HashMap::<_, Vec<_>>::new();
    let mut current_file = None;

    for line in reader.byte_lines() {
        let line = line?;
        if let Some(path) = line.strip_prefix(b"# file: ") {
            current_file = Some(
                String::from_utf8(path.into())
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
            );
        } else if let Some(file) = &current_file {
            // TODO: use slice::split_once when it is stabilized.
            if let Some((key, value)) = line.split_once_str("=") {
                let key = String::from_utf8(key.into())
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                let value = Value::try_from(value)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                result.entry(file.clone()).or_default().push((key, value));
            }
        }
    }
    Ok(result)
}

#[inline]
fn transform_entry<T>(
    entry: NormalEntry<T>,
    name: Option<&str>,
    value: &[u8],
    remove: Option<&str>,
) -> NormalEntry<T> {
    let xattrs = transform_xattr(entry.xattrs(), name, value, remove);
    entry.with_xattrs(xattrs)
}

#[inline]
fn transform_xattr(
    xattrs: &[pna::ExtendedAttribute],
    name: Option<&str>,
    value: &[u8],
    remove: Option<&str>,
) -> Vec<pna::ExtendedAttribute> {
    let mut xattrs = xattrs
        .iter()
        .map(|it| (it.name(), it.value()))
        .collect::<IndexMap<_, _>>();
    if let Some(name) = name {
        xattrs.insert(name, value);
    }
    if let Some(name) = remove {
        xattrs.shift_remove_entry(name);
    }
    xattrs
        .into_iter()
        .map(|(key, value)| pna::ExtendedAttribute::new(key.into(), value.into()))
        .collect()
}

#[derive(thiserror::Error, Clone, Eq, PartialEq, Debug)]
enum ValueError {
    #[error(transparent)]
    InvalidHex(#[from] const_hex::FromHexError),
    #[error(transparent)]
    InvalidBase64(#[from] base64::DecodeError),
    #[error("missing tailing quote")]
    Unclosed,
    #[error("unknown escape character")]
    InvalidEscaped,
}

#[derive(Clone, Default, Eq, PartialEq, Hash, Debug)]
struct Value(Vec<u8>);

impl FromStr for Value {
    type Err = ValueError;

    #[inline]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::try_from(s.as_bytes())
    }
}

impl TryFrom<&[u8]> for Value {
    type Error = ValueError;

    #[inline]
    fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
        Ok(Self(if let Some(stripped) = s.strip_prefix(b"0x") {
            const_hex::decode(stripped)?
        } else if let Some(stripped) = s.strip_prefix(b"0s") {
            base64::engine::general_purpose::STANDARD.decode(stripped)?
        } else if let Some(s) = s.strip_prefix(b"\"") {
            if s.ends_with(b"\\\"") && !s.ends_with(b"\\\\\"") {
                return Err(ValueError::Unclosed);
            } else if let Some(s) = s.strip_suffix(b"\"") {
                unescape_xattr_value_text(s)?
            } else {
                return Err(ValueError::Unclosed);
            }
        } else {
            s.to_vec()
        }))
    }
}

impl Value {
    #[inline]
    fn as_bytes(&self) -> &[u8] {
        &self.0
    }
}

struct DisplayValue<'a> {
    value: &'a [u8],
    encoding: Option<Encoding>,
}

impl<'a> DisplayValue<'a> {
    #[inline]
    const fn new(value: &'a [u8], encoding: Option<Encoding>) -> Self {
        Self { value, encoding }
    }

    #[inline]
    fn fmt_auto(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match std::str::from_utf8(self.value) {
            Ok(_) => self.fmt_text(f),
            Err(_e) => self.fmt_base64(f),
        }
    }

    #[inline]
    fn fmt_text(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_char('"')?;
        Display::fmt(
            &unsafe { String::from_utf8_unchecked(escape_xattr_value_text(self.value)) },
            f,
        )?;
        f.write_char('"')
    }

    #[inline]
    fn fmt_hex(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str("0x")?;
        Display::fmt(&const_hex::display(self.value), f)
    }

    #[inline]
    fn fmt_base64(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str("0s")?;
        f.write_str(&base64::engine::general_purpose::STANDARD.encode(self.value))
    }
}

impl Display for DisplayValue<'_> {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match &self.encoding {
            None => self.fmt_auto(f),
            Some(Encoding::Text) => self.fmt_text(f),
            Some(Encoding::Hex) => self.fmt_hex(f),
            Some(Encoding::Base64) => self.fmt_base64(f),
        }
    }
}

fn escape_xattr_value_text(text: &[u8]) -> Vec<u8> {
    let mut result = Vec::with_capacity(text.len());
    text.iter().for_each(|c| match c {
        b'"' => result.extend_from_slice(b"\\\""),
        b'\\' => result.extend_from_slice(b"\\\\"),
        b'\0' | b'\n' | b'\r' => {
            result.push(b'\\');
            result.push(b'0' + (*c >> 6));
            result.push(b'0' + ((*c & 0o70) >> 3));
            result.push(b'0' + (*c & 0o7));
        }
        _ => result.push(*c),
    });
    result
}

fn unescape_xattr_value_text(text: &[u8]) -> Result<Vec<u8>, ValueError> {
    let mut result = Vec::with_capacity(text.len());
    let mut chars = text.iter().copied();
    while let Some(c) = chars.next() {
        match c {
            b'\\' => {
                if let Some(next_char) = chars.next() {
                    if next_char == b'\\' {
                        result.push(b'\\')
                    } else if next_char == b'"' {
                        result.push(b'"')
                    } else if matches!(next_char, b'0'..=b'7') {
                        let mut unescaped = next_char - b'0';
                        let next_char = chars.next().ok_or(ValueError::InvalidEscaped)?;
                        if matches!(next_char, b'0'..=b'7') {
                            unescaped = (unescaped << 3) + next_char - b'0';
                        }
                        let next_char = chars.next().ok_or(ValueError::InvalidEscaped)?;
                        if matches!(next_char, b'0'..=b'7') {
                            unescaped = (unescaped << 3) + next_char - b'0';
                        }
                        result.push(unescaped);
                    } else {
                        return Err(ValueError::InvalidEscaped);
                    }
                } else {
                    return Err(ValueError::InvalidEscaped);
                }
            }
            _ => result.push(c),
        };
    }
    Ok(result)
}

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

    #[test]
    fn parse_dump_for_restore() {
        assert_eq!(
            parse_dump(
                [
                    "# file: path/to/file1",
                    "user.a=\"abc\"",
                    "user.b=0x0102",
                    "",
                    "# file: path/to/file2",
                    "user.c=0sYWJj",
                ]
                .join("\n")
                .as_bytes()
            )
            .unwrap(),
            maplit::hashmap! {
                "path/to/file1".into() =>
                vec![
                    ("user.a".into(), Value("abc".into())),
                    ("user.b".into(), Value(vec![1, 2])),
                ],
                "path/to/file2".into() =>
                vec![("user.c".into(), Value("abc".into()))],

            }
        );
    }

    #[test]
    fn encode_text() {
        let v = DisplayValue::new(b"abc", Some(Encoding::Text));
        assert_eq!(format!("{v}"), "\"abc\"");
    }

    #[test]
    fn encode_hex() {
        let v = DisplayValue::new(b"abc", Some(Encoding::Hex));
        assert_eq!(format!("{v}"), "0x616263");
    }

    #[test]
    fn encode_base64() {
        let v = DisplayValue::new(b"abc", Some(Encoding::Base64));
        assert_eq!(format!("{v}"), "0sYWJj");
    }

    #[test]
    fn decode_text() {
        assert_eq!(Value(b"abc".into()), Value::from_str("abc").unwrap());
        assert_eq!(Value(b"a\\".into()), Value::from_str("\"a\\\\\"").unwrap());
        assert_eq!(Value(b"".into()), Value::from_str("").unwrap());
    }

    #[test]
    fn decode_hex() {
        assert_eq!(Value(b"abc".into()), Value::from_str("0x616263").unwrap());
        assert_eq!(
            Value([0, 1, 17].into()),
            Value::from_str("0x000111").unwrap()
        );
        assert_eq!(Value(b"".into()), Value::from_str("0x").unwrap());
    }

    #[test]
    fn decode_base64() {
        assert_eq!(Value(b"abc".into()), Value::from_str("0sYWJj").unwrap());
        assert_eq!(Value(b"".into()), Value::from_str("0s").unwrap());
    }

    #[test]
    fn escape_text() {
        assert_eq!(b"".as_slice(), escape_xattr_value_text(b""));
        assert_eq!(b"a\\\\b\\\"".as_slice(), escape_xattr_value_text(b"a\\b\""));
    }

    #[test]
    fn escape_unescape() {
        assert_eq!(
            b"\"\\\n\r\0".as_slice(),
            unescape_xattr_value_text(&escape_xattr_value_text(b"\"\\\n\r\0")).unwrap()
        );
    }

    #[test]
    fn set_xattr() {
        let xattrs = transform_xattr(&[], Some("key"), b"value", None);

        assert_eq!(
            xattrs,
            vec![pna::ExtendedAttribute::new("key".into(), b"value".into()),]
        );
    }

    #[test]
    fn overwrite_xattr() {
        let xattrs = transform_xattr(
            &[pna::ExtendedAttribute::new("key".into(), b"origin".into())],
            Some("key"),
            b"value",
            None,
        );

        assert_eq!(
            xattrs,
            vec![pna::ExtendedAttribute::new("key".into(), b"value".into()),]
        );
    }

    #[test]
    fn remove_xattr() {
        let xattrs = transform_xattr(
            &[pna::ExtendedAttribute::new("key".into(), b"origin".into())],
            None,
            b"value",
            Some("key"),
        );

        assert_eq!(xattrs, vec![]);
    }
}
