use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    program::set_return_data,
    program_error::ProgramError,
    pubkey::Pubkey,
};

mod wordlist {
    include!(concat!(env!("OUT_DIR"), "/generated_dictionary.rs"));
}

pub const INSTRUCTION_CLASSIFY_LOGIN: u8 = 1;
pub const CLASS_FREE: u32 = 0;
pub const CLASS_PREMIUM: u32 = 1;
pub const CLASS_TRADEMARK: u32 = 2;
pub const MAX_WORDS_PER_LOGIN: usize = 3;
pub const MAX_LOGIN_LEN: usize = 20;

solana_program::declare_id!("3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo");

entrypoint!(process_instruction);

pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let class = match parse_instruction(instruction_data)? {
        Instruction::ClassifyLogin { login } => classify(&login),
    };
    set_return_data(&class.to_le_bytes());
    Ok(())
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Instruction {
    ClassifyLogin { login: String },
}

fn parse_instruction(data: &[u8]) -> Result<Instruction, ProgramError> {
    let (&tag, rest) = data
        .split_first()
        .ok_or(ProgramError::InvalidInstructionData)?;
    match tag {
        INSTRUCTION_CLASSIFY_LOGIN => {
            let login = read_string_u32(rest)?;
            Ok(Instruction::ClassifyLogin { login })
        }
        _ => Err(ProgramError::InvalidInstructionData),
    }
}

fn read_string_u32(data: &[u8]) -> Result<String, ProgramError> {
    if data.len() < 4 {
        return Err(ProgramError::InvalidInstructionData);
    }
    let len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
    let body = data
        .get(4..4 + len)
        .ok_or(ProgramError::InvalidInstructionData)?;
    std::str::from_utf8(body)
        .map(|s| s.to_string())
        .map_err(|_| ProgramError::InvalidInstructionData)
}

pub fn build_classify_login_instruction_data(login: &str) -> Vec<u8> {
    let bytes = login.as_bytes();
    let mut out = Vec::with_capacity(1 + 4 + bytes.len());
    out.push(INSTRUCTION_CLASSIFY_LOGIN);
    out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
    out.extend_from_slice(bytes);
    out
}

pub fn classify(login: &str) -> u32 {
    let Some(normalized) = normalize_login(login) else {
        return CLASS_PREMIUM;
    };
    if let Some(v) = classify_split(&normalized) {
        return v;
    }
    if normalized.len() <= 7 {
        return CLASS_PREMIUM;
    }
    CLASS_FREE
}

fn normalize_login(login: &str) -> Option<String> {
    if login.is_empty() || login.len() > MAX_LOGIN_LEN {
        return None;
    }
    let mut out = String::with_capacity(login.len());
    for ch in login.chars() {
        if ch == '_' {
            continue;
        }
        if !ch.is_ascii_alphanumeric() {
            return None;
        }
        out.push(ch.to_ascii_lowercase());
    }
    if out.is_empty() || out.len() > MAX_LOGIN_LEN {
        return None;
    }
    Some(out)
}

fn classify_split(login: &str) -> Option<u32> {
    fn dfs(rest: &str, depth: usize, has_tm: bool) -> Option<u32> {
        if rest.is_empty() {
            if depth > 0 && depth <= MAX_WORDS_PER_LOGIN {
                return Some(if has_tm { CLASS_TRADEMARK } else { CLASS_PREMIUM });
            }
            return None;
        }
        if depth >= MAX_WORDS_PER_LOGIN {
            return None;
        }
        let max_piece = rest.len().min(MAX_LOGIN_LEN);
        let mut premium_found = false;
        for i in 1..=max_piece {
            let candidate = &rest[..i];
            let is_tm = is_trademark_word(candidate);
            let is_pr = is_tm || is_premium_word(candidate);
            if !is_pr {
                continue;
            }
            match dfs(&rest[i..], depth + 1, has_tm || is_tm) {
                Some(CLASS_TRADEMARK) => return Some(CLASS_TRADEMARK),
                Some(CLASS_PREMIUM) => premium_found = true,
                _ => {}
            }
        }
        if premium_found {
            Some(CLASS_PREMIUM)
        } else {
            None
        }
    }
    dfs(login, 0, false)
}

fn is_premium_word(word: &str) -> bool {
    wordlist::PREMIUM_WORDS.binary_search(&word).is_ok()
}

fn is_trademark_word(word: &str) -> bool {
    wordlist::TRADEMARK_WORDS.binary_search(&word).is_ok()
}

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

    #[test]
    fn normalize_removes_underscores_and_lowercases() {
        assert_eq!(normalize_login("Ab_C1"), Some("abc1".to_string()));
    }

    #[test]
    fn normalize_rejects_invalid_chars() {
        assert_eq!(normalize_login("ab-c"), None);
    }

    #[test]
    fn short_unknown_login_is_premium() {
        assert_eq!(classify("abc123"), CLASS_PREMIUM);
    }

    #[test]
    fn long_unknown_login_is_free() {
        assert_eq!(classify("abcdefgh9"), CLASS_FREE);
    }

    #[test]
    fn instruction_roundtrip() {
        let data = build_classify_login_instruction_data("Alice_1");
        let parsed = parse_instruction(&data).unwrap();
        assert_eq!(parsed, Instruction::ClassifyLogin { login: "Alice_1".to_string() });
    }
}
