use super::super::*;

pub(in super::super) fn bytea_decode_escape(input: &str) -> Result<Vec<u8>> {
    fn invalid_bytea_input() -> Error {
        Error::Other("invalid input syntax for type bytea".to_owned())
    }

    let bytes = input.as_bytes();
    let mut out = Vec::with_capacity(bytes.len());
    let mut idx = 0usize;
    while idx < bytes.len() {
        if bytes[idx] != b'\\' {
            out.push(bytes[idx]);
            idx += 1;
            continue;
        }

        if idx + 1 >= bytes.len() {
            return Err(invalid_bytea_input());
        }

        match bytes[idx + 1] {
            b'\\' => {
                out.push(b'\\');
                idx += 2;
            }
            b'0'..=b'7' => {
                if idx + 3 >= bytes.len()
                    || !matches!(bytes[idx + 2], b'0'..=b'7')
                    || !matches!(bytes[idx + 3], b'0'..=b'7')
                {
                    return Err(invalid_bytea_input());
                }
                let oct = u16::from(bytes[idx + 1] - b'0') << 6
                    | u16::from(bytes[idx + 2] - b'0') << 3
                    | u16::from(bytes[idx + 3] - b'0');
                if oct > u16::from(u8::MAX) {
                    return Err(invalid_bytea_input());
                }
                out.push(oct as u8);
                idx += 4;
            }
            _ => return Err(invalid_bytea_input()),
        }
    }
    Ok(out)
}

fn pg_function_type_name(value: &StoredValue) -> &'static str {
    match value {
        StoredValue::Null => "unknown",
        StoredValue::Boolean(_) => "boolean",
        StoredValue::Int32(_) => "integer",
        StoredValue::Int64(_) => "bigint",
        StoredValue::Float64(_) => "double precision",
        StoredValue::Text(_) => "text",
        StoredValue::Bytes(_) => "bytea",
        StoredValue::Timestamp(_) => "timestamp without time zone",
        StoredValue::Interval(_) => "interval",
        StoredValue::Uuid(_) => "uuid",
        StoredValue::Array(_) => "array",
        StoredValue::Vector(_) => "vector",
        StoredValue::Json(_) => "json",
        StoredValue::Jsonb(_) => "jsonb",
        StoredValue::Time(_) => "time without time zone",
        StoredValue::Date(_) => "date",
        StoredValue::Numeric(_) => "numeric",
        StoredValue::Tsvector(_) => "tsvector",
        StoredValue::Tsquery(_) => "tsquery",
    }
}

fn function_not_found(function_name: &str, arg_types: impl Into<String>) -> Error {
    Error::Other(format!(
        "function {}({}) does not exist",
        function_name,
        arg_types.into()
    ))
}

pub(in super::super) fn eval_md5_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    eval_unary_function(function_name, args, |value| {
        let bytes = match value {
            StoredValue::Null => return Ok(StoredValue::Null),
            StoredValue::Text(value) => value.into_bytes(),
            StoredValue::Bytes(value) => value,
            other => {
                return Err(function_not_found(
                    function_name,
                    pg_function_type_name(&other),
                ));
            }
        };
        let digest = format!("{:x}", md5::compute(bytes));
        check_output_byte_size(digest.len())?;
        Ok(StoredValue::Text(digest))
    })
}

pub(in super::super) fn eval_sha256_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    use sha2::{Digest, Sha256};

    eval_unary_function(function_name, args, |value| {
        let bytes = match value {
            StoredValue::Null => return Ok(StoredValue::Null),
            StoredValue::Bytes(value) => value,
            other => {
                return Err(function_not_found(
                    function_name,
                    pg_function_type_name(&other),
                ));
            }
        };
        Ok(StoredValue::Bytes(Sha256::digest(bytes).to_vec()))
    })
}

fn pg_quoted_symbol(ch: char) -> String {
    format!("\"{}\"", ch.escape_default())
}

fn pg_quoted_symbol_str(value: &str) -> String {
    format!("\"{}\"", value.escape_default())
}

fn pg_unknown_encoding_error(fmt: &str) -> Error {
    Error::Other(format!(
        "unrecognized encoding: {}",
        pg_quoted_symbol_str(fmt)
    ))
}

fn pg_unknown_digest_algorithm_error(algorithm: &str) -> Error {
    Error::Other(format!(
        "Cannot use {}: No such hash algorithm",
        pg_quoted_symbol_str(algorithm)
    ))
}

fn pg_base64_value(ch: char) -> Option<u8> {
    match ch {
        'A'..='Z' => Some(ch as u8 - b'A'),
        'a'..='z' => Some(ch as u8 - b'a' + 26),
        '0'..='9' => Some(ch as u8 - b'0' + 52),
        '+' => Some(62),
        '/' => Some(63),
        _ => None,
    }
}

fn base64_decoded_output_upper_bound(data: &str) -> usize {
    let non_whitespace_len = data.chars().filter(|ch| !ch.is_ascii_whitespace()).count();
    non_whitespace_len.saturating_add(3) / 4 * 3
}

fn hex_decoded_output_upper_bound(data: &str) -> usize {
    data.chars().filter(|ch| !ch.is_ascii_whitespace()).count() / 2
}

fn pg_decode_base64(data: &str) -> Result<Vec<u8>> {
    let output_len = base64_decoded_output_upper_bound(data);
    check_output_byte_size(output_len)?;

    let mut out = Vec::with_capacity(output_len);
    let mut buf = 0_u32;
    let mut pos = 0;
    let mut end = 0;
    let mut seen_padded_block = false;

    for ch in data.chars() {
        if seen_padded_block {
            match ch {
                ' ' | '\t' | '\n' | '\r' => continue,
                _ => return Err(Error::Other("invalid base64 end sequence".to_owned())),
            }
        }

        let value = match ch {
            ' ' | '\t' | '\n' | '\r' => continue,
            '=' => {
                if end == 0 {
                    if pos == 2 {
                        end = 1;
                    } else if pos == 3 {
                        end = 2;
                    } else {
                        return Err(Error::Other(
                            "unexpected \"=\" while decoding base64 sequence".to_owned(),
                        ));
                    }
                }
                0
            }
            _ => pg_base64_value(ch).ok_or_else(|| {
                Error::Other(format!(
                    "invalid symbol {} found while decoding base64 sequence",
                    pg_quoted_symbol(ch)
                ))
            })?,
        };

        buf = (buf << 6) + u32::from(value);
        pos += 1;
        if pos == 4 {
            out.push(((buf >> 16) & 255) as u8);
            if end == 0 || end > 1 {
                out.push(((buf >> 8) & 255) as u8);
            }
            if end == 0 || end > 2 {
                out.push((buf & 255) as u8);
            }
            seen_padded_block = end != 0;
            buf = 0;
            pos = 0;
            end = 0;
        }
    }

    if pos != 0 {
        return Err(Error::Other("invalid base64 end sequence".to_owned()));
    }

    Ok(out)
}

fn pg_decode_hex_error(err: hex::FromHexError) -> Error {
    match err {
        hex::FromHexError::InvalidHexCharacter { c, .. } => Error::Other(format!(
            "invalid hexadecimal digit: {}",
            pg_quoted_symbol(c)
        )),
        hex::FromHexError::OddLength => {
            Error::Other("invalid hexadecimal data: odd number of digits".to_owned())
        }
        hex::FromHexError::InvalidStringLength => {
            Error::Other("invalid hexadecimal data: invalid string length".to_owned())
        }
    }
}

fn pg_decode_hex(data: &str) -> Result<Vec<u8>> {
    let output_len = hex_decoded_output_upper_bound(data);
    check_output_byte_size(output_len)?;

    let mut hex_input = String::with_capacity(output_len.saturating_mul(2));
    let mut digits_in_pair = 0usize;

    for ch in data.chars() {
        if ch.is_ascii_whitespace() {
            if digits_in_pair == 1 {
                return Err(Error::Other(format!(
                    "invalid hexadecimal digit: {}",
                    pg_quoted_symbol(ch)
                )));
            }
            continue;
        }

        if !ch.is_ascii_hexdigit() {
            return Err(Error::Other(format!(
                "invalid hexadecimal digit: {}",
                pg_quoted_symbol(ch)
            )));
        }

        hex_input.push(ch);
        digits_in_pair = (digits_in_pair + 1) % 2;
    }

    if digits_in_pair == 1 {
        return Err(Error::Other(
            "invalid hexadecimal data: odd number of digits".to_owned(),
        ));
    }

    hex::decode(hex_input).map_err(pg_decode_hex_error)
}

pub(in super::super) fn eval_decode_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    if args.len() != 2 {
        return Err(Error::Other(format!(
            "DB9 function '{}' expects 2 args, got {}",
            function_name,
            args.len()
        )));
    }

    let mut iter = args.into_iter();
    let data = match iter.next().unwrap() {
        StoredValue::Null => return Ok(StoredValue::Null),
        StoredValue::Text(value) => value,
        other => stored_value_to_string(&other)?,
    };
    let fmt = match iter.next().unwrap() {
        StoredValue::Null => return Ok(StoredValue::Null),
        StoredValue::Text(value) => value,
        other => stored_value_to_string(&other)?,
    };

    let out = if fmt.eq_ignore_ascii_case("base64") {
        pg_decode_base64(&data)?
    } else if fmt.eq_ignore_ascii_case("hex") {
        pg_decode_hex(&data)?
    } else if fmt.eq_ignore_ascii_case("escape") {
        check_output_byte_size(data.len())?;
        bytea_decode_escape(&data)?
    } else {
        return Err(pg_unknown_encoding_error(&fmt));
    };
    Ok(StoredValue::Bytes(out))
}

pub(in super::super) fn eval_digest_function(
    function_name: &str,
    args: Vec<StoredValue>,
) -> Result<StoredValue> {
    if args.len() != 2 {
        return Err(Error::Other(format!(
            "DB9 function '{}' expects 2 args, got {}",
            function_name,
            args.len()
        )));
    }

    let mut iter = args.into_iter();
    let data = match iter.next().unwrap() {
        StoredValue::Null => return Ok(StoredValue::Null),
        StoredValue::Bytes(value) => value,
        StoredValue::Text(value) => value.into_bytes(),
        other => {
            return Err(function_not_found(
                function_name,
                format!("{}, text", pg_function_type_name(&other)),
            ));
        }
    };
    let algorithm = match iter.next().unwrap() {
        StoredValue::Null => return Ok(StoredValue::Null),
        StoredValue::Text(value) => value,
        other => {
            return Err(function_not_found(
                function_name,
                format!("{}, {}", "bytea", pg_function_type_name(&other)),
            ));
        }
    };
    let algorithm_lower = algorithm.to_ascii_lowercase();

    let digest = match algorithm_lower.as_str() {
        "md5" => md5::compute(&data).0.to_vec(),
        "sha1" => {
            use sha1::{Digest as Sha1Digest, Sha1};
            Sha1::digest(&data).to_vec()
        }
        "sha224" => {
            use sha2::{Digest, Sha224};
            Sha224::digest(&data).to_vec()
        }
        "sha256" => {
            use sha2::{Digest, Sha256};
            Sha256::digest(&data).to_vec()
        }
        "sha384" => {
            use sha2::{Digest, Sha384};
            Sha384::digest(&data).to_vec()
        }
        "sha512" => {
            use sha2::{Digest, Sha512};
            Sha512::digest(&data).to_vec()
        }
        _ => return Err(pg_unknown_digest_algorithm_error(&algorithm)),
    };

    Ok(StoredValue::Bytes(digest))
}

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

    #[test]
    fn decode_matches_pg_base64_and_hex_edge_semantics() {
        assert_eq!(
            eval_decode_function(
                "decode",
                vec![
                    StoredValue::Text("QQ==".to_owned()),
                    StoredValue::Text("base64".to_owned()),
                ],
            )
            .unwrap(),
            StoredValue::Bytes(vec![b'A'])
        );
        assert_eq!(
            eval_decode_function(
                "decode",
                vec![
                    StoredValue::Text("QQ=Q".to_owned()),
                    StoredValue::Text("base64".to_owned()),
                ],
            )
            .unwrap(),
            StoredValue::Bytes(vec![b'A'])
        );
        for input in ["QQ==AA==", "QQ==A===", "QQ==AAAA"] {
            assert_eq!(
                eval_decode_function(
                    "decode",
                    vec![
                        StoredValue::Text(input.to_owned()),
                        StoredValue::Text("base64".to_owned()),
                    ],
                )
                .unwrap_err()
                .to_string(),
                "invalid base64 end sequence"
            );
        }
        assert_eq!(
            eval_decode_function(
                "decode",
                vec![
                    StoredValue::Text("\\x61".to_owned()),
                    StoredValue::Text("hex".to_owned()),
                ],
            )
            .unwrap_err()
            .to_string(),
            "invalid hexadecimal digit: \"\\\\\""
        );
        assert_eq!(
            eval_decode_function(
                "decode",
                vec![
                    StoredValue::Text(" 61 ".to_owned()),
                    StoredValue::Text("hex".to_owned()),
                ],
            )
            .unwrap(),
            StoredValue::Bytes(vec![0x61])
        );
        assert_eq!(
            eval_decode_function(
                "decode",
                vec![
                    StoredValue::Text("61 62".to_owned()),
                    StoredValue::Text("hex".to_owned()),
                ],
            )
            .unwrap(),
            StoredValue::Bytes(b"ab".to_vec())
        );
        assert_eq!(
            eval_decode_function(
                "decode",
                vec![
                    StoredValue::Text("6 1".to_owned()),
                    StoredValue::Text("hex".to_owned()),
                ],
            )
            .unwrap_err()
            .to_string(),
            "invalid hexadecimal digit: \" \""
        );
        assert_eq!(
            eval_decode_function(
                "decode",
                vec![
                    StoredValue::Text("QQ=".to_owned()),
                    StoredValue::Text("base64".to_owned()),
                ],
            )
            .unwrap_err()
            .to_string(),
            "invalid base64 end sequence"
        );
        for input in ["QQ====", "QQ==AA"] {
            assert_eq!(
                eval_decode_function(
                    "decode",
                    vec![
                        StoredValue::Text(input.to_owned()),
                        StoredValue::Text("base64".to_owned()),
                    ],
                )
                .unwrap_err()
                .to_string(),
                "invalid base64 end sequence"
            );
        }
        assert_eq!(
            eval_decode_function(
                "decode",
                vec![
                    StoredValue::Text("Q=Q=".to_owned()),
                    StoredValue::Text("base64".to_owned()),
                ],
            )
            .unwrap_err()
            .to_string(),
            "unexpected \"=\" while decoding base64 sequence"
        );
    }

    #[test]
    fn decode_and_digest_keep_pg_error_messages() {
        assert_eq!(
            eval_decode_function(
                "decode",
                vec![
                    StoredValue::Text("***".to_owned()),
                    StoredValue::Text("base64".to_owned()),
                ],
            )
            .unwrap_err()
            .to_string(),
            "invalid symbol \"*\" found while decoding base64 sequence"
        );
        assert_eq!(
            eval_decode_function(
                "decode",
                vec![
                    StoredValue::Text("616263".to_owned()),
                    StoredValue::Text(" hex ".to_owned()),
                ],
            )
            .unwrap_err()
            .to_string(),
            "unrecognized encoding: \" hex \""
        );
        assert_eq!(
            eval_digest_function(
                "digest",
                vec![
                    StoredValue::Text("hello".to_owned()),
                    StoredValue::Text(" sha256 ".to_owned()),
                ],
            )
            .unwrap_err()
            .to_string(),
            "Cannot use \" sha256 \": No such hash algorithm"
        );
    }

    #[test]
    fn decode_output_size_projection_is_bounded() {
        assert_eq!(base64_decoded_output_upper_bound("AAAA"), 3);
        assert_eq!(base64_decoded_output_upper_bound("A A\nA\tA"), 3);
        assert_eq!(hex_decoded_output_upper_bound("AB CD"), 2);
        check_output_byte_size(DEFAULT_MAX_STRING_OUTPUT_BYTES).unwrap();

        let err = check_output_byte_size(DEFAULT_MAX_STRING_OUTPUT_BYTES + 1).unwrap_err();
        assert!(err.to_string().contains("requested length too large"));
    }

    #[test]
    fn md5_output_size_projection_is_bounded() {
        with_db9_string_output_byte_limit(32, || {
            eval_md5_function("md5", vec![StoredValue::Text("hello".to_owned())])
        })
        .unwrap();

        let err = with_db9_string_output_byte_limit(31, || {
            eval_md5_function("md5", vec![StoredValue::Text("hello".to_owned())])
        })
        .unwrap_err();
        assert!(err.to_string().contains("requested length too large"));
    }

    #[test]
    fn hash_functions_reject_pg_unsupported_overloads() {
        assert_eq!(
            eval_md5_function("md5", vec![StoredValue::Int32(7)])
                .unwrap_err()
                .to_string(),
            "function md5(integer) does not exist"
        );
        assert_eq!(
            eval_sha256_function("sha256", vec![StoredValue::Text("hello".to_owned())])
                .unwrap_err()
                .to_string(),
            "function sha256(text) does not exist"
        );
        assert_eq!(
            eval_digest_function(
                "digest",
                vec![
                    StoredValue::Int32(7),
                    StoredValue::Text("sha256".to_owned())
                ],
            )
            .unwrap_err()
            .to_string(),
            "function digest(integer, text) does not exist"
        );
    }

    #[test]
    fn decode_escape_matches_pg18_strict_semantics() {
        assert_eq!(bytea_decode_escape(r"\123").unwrap(), vec![0o123]);
        assert_eq!(bytea_decode_escape(r"\\").unwrap(), br"\".to_vec());
        for input in [r"\1", r"\12", r"\400", r"\777", r"\x", r"\", r"\8", r"\999"] {
            assert_eq!(
                bytea_decode_escape(input).unwrap_err().to_string(),
                "invalid input syntax for type bytea"
            );
        }
    }
}
