/// Dean Edwards p,a,c,k,e,d unpacker.
///
/// Mirrors the original JS decoder:
///   while(c--) if(k[c]) p = p.replace(/\b{c.toString(a)}\b/g, k[c]);
///
/// Usage:
///   let source = r#"eval(function(p,a,c,k,e,d){...}('...',36,N,'w0|w1|...'.split('|'),0,{}))"#;
///   let plain  = unpack(source)?;

use std::fmt;

// ── Error type ────────────────────────────────────────────────────────────────

#[derive(Debug)]
pub enum UnpackError {
    NotPacked,
    MalformedArgs(String),
    BadBase(u32),
}

impl fmt::Display for UnpackError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NotPacked => write!(f, "input does not look like a packed script"),
            Self::MalformedArgs(s) => write!(f, "could not parse packed arguments: {s}"),
            Self::BadBase(b) => write!(f, "unsupported base {b} (supported: 2–62)"),
        }
    }
}

impl std::error::Error for UnpackError {}

// ── Public entry point ────────────────────────────────────────────────────────

/// Detect, parse, and unpack a Dean Edwards packed script.
/// Returns the deobfuscated source on success.
pub fn unpack(input: &str) -> Result<String, UnpackError> {
    let args = extract_args(input.trim())?;
    decode(args)
}

// ── Argument extraction ───────────────────────────────────────────────────────

struct PackedArgs {
    payload: String, // p
    base:    u32,    // a
    words:   Vec<String>, // k  (already split)
}

/// Find the argument list of the outer `function(p,a,c,k,e,d){...}(...)` call
/// and parse p, a, c, k from it.
fn extract_args(input: &str) -> Result<PackedArgs, UnpackError> {
    // Locate the opening of the argument tuple: the '(' that directly follows
    // the closing '}' of the function body.
    let body_end = input
        .find("}('")           // most common: }('payload',…
        .or_else(|| input.find("}(\""))
        .ok_or(UnpackError::NotPacked)?;

    // args_start points at '(' – skip it to reach the opening quote of the payload.
    let args_start = body_end + 1;
    let args_str = input[args_start..].trim_start_matches('(');

    // Pull the payload string (first argument).
    let (payload, after_payload) = parse_js_string(args_str)
        .ok_or_else(|| UnpackError::MalformedArgs("could not read payload string".into()))?;

    // Expect ',base,count,'
    let rest = after_payload
        .trim_start_matches(',');

    let (base_str, rest) = rest
        .split_once(',')
        .ok_or_else(|| UnpackError::MalformedArgs("missing base".into()))?;

    let base: u32 = base_str
        .trim()
        .parse()
        .map_err(|_| UnpackError::MalformedArgs(format!("bad base: {base_str}")))?;

    let (count_str, rest) = rest
        .split_once(',')
        .ok_or_else(|| UnpackError::MalformedArgs("missing count".into()))?;

    let count: usize = count_str
        .trim()
        .parse()
        .map_err(|_| UnpackError::MalformedArgs(format!("bad count: {count_str}")))?;

    // Parse the dictionary k.  Two common forms:
    //   'w0|w1|w2'.split('|')
    //   ["w0","w1","w2"]
    let words = parse_dictionary(rest.trim(), count)
        .ok_or_else(|| UnpackError::MalformedArgs("could not parse dictionary".into()))?;

    Ok(PackedArgs { payload, base, words })
}

// ── Dictionary parser ─────────────────────────────────────────────────────────

fn parse_dictionary(s: &str, count: usize) -> Option<Vec<String>> {
    if s.starts_with('\'') || s.starts_with('"') {
        // 'w0|w1|…'.split('|')  – separator can be any single char
        let (joined, rest) = parse_js_string(s)?;
        // find .split('<sep>')
        let sep_start = rest.find(".split(")?;
        let after_split = &rest[sep_start + 7..]; // skip `.split(`
        let (sep, _) = parse_js_string(after_split.trim())?;
        let sep_char = if sep.is_empty() { '|' } else { sep.chars().next().unwrap() };
        let words: Vec<String> = joined.split(sep_char).map(str::to_owned).collect();
        Some(pad_to(words, count))
    } else if s.starts_with('[') {
        // ["w0","w1",…]
        let end = s.find(']')?;
        let inner = &s[1..end];
        let words = parse_array_literal(inner);
        Some(pad_to(words, count))
    } else {
        None
    }
}

/// Parse a JS array literal (no nesting needed for the k array).
fn parse_array_literal(s: &str) -> Vec<String> {
    let mut words = Vec::new();
    let mut rest = s.trim();
    loop {
        rest = rest.trim_start_matches(',').trim();
        if rest.is_empty() { break; }
        if rest.starts_with('\'') || rest.starts_with('"') {
            if let Some((w, after)) = parse_js_string(rest) {
                words.push(w);
                rest = after.trim();
            } else {
                break;
            }
        } else {
            // empty slot  →  push empty string
            if let Some(pos) = rest.find(',') {
                words.push(String::new());
                rest = &rest[pos..];
            } else {
                break;
            }
        }
    }
    words
}

fn pad_to(mut v: Vec<String>, n: usize) -> Vec<String> {
    v.resize(n, String::new());
    v
}

// ── JS string parser ──────────────────────────────────────────────────────────

/// Parse a single-quoted or double-quoted JS string literal at the start of `s`.
/// Returns (unescaped content, remainder after closing quote).
fn parse_js_string(s: &str) -> Option<(String, &str)> {
    let mut chars = s.char_indices();
    let (_, quote) = chars.next()?;
    if quote != '\'' && quote != '"' { return None; }

    let mut result = String::new();
    let mut escaped = false;

    for (i, ch) in chars {
        if escaped {
            match ch {
                'n'  => result.push('\n'),
                'r'  => result.push('\r'),
                't'  => result.push('\t'),
                '\\' => result.push('\\'),
                '\'' => result.push('\''),
                '"'  => result.push('"'),
                _    => { result.push('\\'); result.push(ch); }
            }
            escaped = false;
        } else if ch == '\\' {
            escaped = true;
        } else if ch == quote {
            return Some((result, &s[i + ch.len_utf8()..]));
        } else {
            result.push(ch);
        }
    }
    None // unclosed string
}

// ── Number → string for arbitrary base ───────────────────────────────────────

/// JavaScript's `Number.prototype.toString(radix)` for bases 2–62.
/// Digits: 0-9, then a-z (10-35), then A-Z (36-61).
fn num_to_base_str(mut n: usize, base: u32) -> Result<String, UnpackError> {
    if base < 2 || base > 62 {
        return Err(UnpackError::BadBase(base));
    }
    if n == 0 { return Ok("0".to_owned()); }

    const DIGITS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    let base = base as usize;
    let mut buf = Vec::new();
    while n > 0 {
        buf.push(DIGITS[n % base] as char);
        n /= base;
    }
    buf.reverse();
    Ok(buf.into_iter().collect())
}

// ── Core decode ───────────────────────────────────────────────────────────────

/// JS regex `\b` word-boundary replacement.
/// We use a hand-rolled matcher so we don't need an external crate.
fn replace_word_boundary(haystack: &str, needle: &str, replacement: &str) -> String {
    if needle.is_empty() { return haystack.to_owned(); }

    let h: Vec<char> = haystack.chars().collect();
    let n: Vec<char> = needle.chars().collect();
    let nlen = n.len();
    let hlen = h.len();

    let is_word = |c: char| c.is_ascii_alphanumeric() || c == '_';

    let mut out = String::with_capacity(haystack.len());
    let mut i = 0;

    while i <= hlen.saturating_sub(nlen) {
        // Check if h[i..i+nlen] == needle
        if h[i..i + nlen] == n[..] {
            // Word-boundary checks
            let left_ok  = i == 0 || !is_word(h[i - 1]);
            let right_ok = i + nlen == hlen || !is_word(h[i + nlen]);

            if left_ok && right_ok {
                out.push_str(replacement);
                i += nlen;
                continue;
            }
        }
        out.push(h[i]);
        i += 1;
    }
    // Append whatever is left
    for ch in &h[i..] { out.push(*ch); }
    out
}

fn decode(args: PackedArgs) -> Result<String, UnpackError> {
    let PackedArgs { mut payload, base, words } = args;

    // Validate base once up front.
    if base < 2 || base > 62 {
        return Err(UnpackError::BadBase(base));
    }

    // Mirror the JS: for c from (words.len()-1) down to 0
    let count = words.len();
    for c in (0..count).rev() {
        if words[c].is_empty() { continue; }
        let key = num_to_base_str(c, base)?;
        payload = replace_word_boundary(&payload, &key, &words[c]);
    }

    Ok(payload)
}

// ── Tests ─────────────────────────────────────────────────────────────────────

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

    /// Minimal self-contained packed snippet (base 36, 3 words).
    /// Original source: `var hello = world + foo;`
    /// Packed manually for the test:
    ///   payload  = "var 1 = 2 + 0;"
    ///   base     = 36
    ///   count    = 3
    ///   words    = ["foo", "hello", "world"]   (indices 0,1,2)
    #[test]
    fn test_basic_unpack() {
        // Build a fake packed string without the eval wrapper for direct testing.
        let args = PackedArgs {
            payload: "var 1 = 2 + 0;".to_owned(),
            base:    36,
            words:   vec!["foo".to_owned(), "hello".to_owned(), "world".to_owned()],
        };
        let result = decode(args).unwrap();
        assert_eq!(result, "var hello = world + foo;");
    }

    #[test]
    fn test_word_boundary() {
        // "foo10bar" should NOT replace "10", but " 10 " should.
        let result = replace_word_boundary("foo10bar baz 10 qux10", "10", "X");
        assert_eq!(result, "foo10bar baz X qux10");
    }

    #[test]
    fn test_num_to_base_str() {
        assert_eq!(num_to_base_str(0,  36).unwrap(), "0");
        assert_eq!(num_to_base_str(10, 36).unwrap(), "a");
        assert_eq!(num_to_base_str(35, 36).unwrap(), "z");
        assert_eq!(num_to_base_str(36, 36).unwrap(), "10");
        assert_eq!(num_to_base_str(255, 16).unwrap(), "ff");
        assert_eq!(num_to_base_str(7,  2).unwrap(),  "111");
    }

    #[test]
    fn test_parse_split_dictionary() {
        let input = r#"'foo|bar|baz'.split('|'),0,{})"#;
        let words = parse_dictionary(input, 3).unwrap();
        assert_eq!(words, vec!["foo", "bar", "baz"]);
    }

    #[test]
    fn test_parse_array_dictionary() {
        let input = r#"["alpha","","gamma"],0,{})"#;
        let words = parse_dictionary(input, 3).unwrap();
        assert_eq!(words[0], "alpha");
        assert_eq!(words[1], "");
        assert_eq!(words[2], "gamma");
    }

    /// Full round-trip with the eval wrapper (base 10, tiny example).
    #[test]
    fn test_full_eval_wrapper() {
        // payload: "0 1 2"  words: ["hello","world","rust"]  base:10  count:3
        let packed = r#"eval(function(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}('0 1 2',10,3,'hello|world|rust'.split('|'),0,{}))"#;
        let result = unpack(packed).unwrap();
        assert_eq!(result, "hello world rust");
    }
}