//! Structural, recompilation-invariant fingerprint of a function — integer features only.
//!
//! Walks the control-flow graph from the entry point (bounded), decoding with iced-x86, and
//! summarises shape into counts that survive a recompile: instruction/block/call/branch counts,
//! distinct callees, RIP-relative data references, and a mnemonic-category histogram. No
//! addresses or immediates enter the vector (those change every build); only structure does.

use crate::elf::CodeImage;
use iced_x86::{Decoder, DecoderOptions, FlowControl, Instruction, Mnemonic, OpKind};
use std::collections::HashSet;

pub const NCATS: usize = 18;
/// Hash buckets for the referenced-string content sketch.
pub const NREF: usize = 32;

#[derive(Default, Clone)]
pub struct Fingerprint {
    pub size_bytes: u32,
    pub insns: u32,
    pub blocks: u32,
    pub calls: u32,
    pub distinct_callees: u32,
    pub cond_branches: u32,
    pub uncond_branches: u32,
    pub rets: u32,
    pub data_refs: u32,
    pub indirect: u32,
    // call-graph context: aggregate shape of this function's callees (a callee's structure is
    // build-invariant), which distinguishes otherwise-identical thunks by which function they hit.
    pub ctx_ins: u32,
    pub ctx_calls: u32,
    pub ctx_br: u32,
    pub cats: [u32; NCATS],
    // content sketch: hash-bucket histogram of the printable rodata strings this function
    // references. The string *content* is build-invariant and highly function-specific, so it adds
    // discriminative signal the pure structural counts lack.
    pub refs: [u32; NREF],
}

impl Fingerprint {
    pub fn to_vec(&self) -> Vec<u32> {
        let mut v = vec![
            self.size_bytes,
            self.insns,
            self.blocks,
            self.calls,
            self.distinct_callees,
            self.cond_branches,
            self.uncond_branches,
            self.rets,
            self.data_refs,
            self.indirect,
            self.ctx_ins,
            self.ctx_calls,
            self.ctx_br,
        ];
        v.extend_from_slice(&self.cats);
        v.extend_from_slice(&self.refs);
        v
    }
}

/// If `target` points at a printable C string in read-only data, hash it into a `[0, NREF)` bucket.
/// Only strings of a few printable chars count — this skips jump tables and pointer arrays (whose
/// bytes are addresses that move across builds), keeping the sketch build-invariant.
fn string_bucket(img: &CodeImage, target: u64) -> Option<usize> {
    let bytes = img.rodata_at(target, 32)?;
    let run: &[u8] = {
        let end = bytes
            .iter()
            .position(|&c| !(0x20..0x7f).contains(&c))
            .unwrap_or(bytes.len());
        &bytes[..end]
    };
    if run.len() < 3 {
        return None;
    }
    // FNV-1a over the printable run
    let mut h: u64 = 0xcbf29ce484222325;
    for &b in run {
        h = (h ^ b as u64).wrapping_mul(0x100000001b3);
    }
    Some((h % NREF as u64) as usize)
}

fn category(insn: &Instruction) -> usize {
    match insn.flow_control() {
        FlowControl::Call | FlowControl::IndirectCall => return 10,
        FlowControl::UnconditionalBranch | FlowControl::IndirectBranch => return 11,
        FlowControl::ConditionalBranch => return 12,
        FlowControl::Return => return 13,
        _ => {}
    }
    if insn.op0_register().is_xmm()
        || insn.op0_register().is_ymm()
        || insn.op1_register().is_xmm()
        || insn.op1_register().is_ymm()
    {
        return 17;
    }
    use Mnemonic::*;
    match insn.mnemonic() {
        Mov | Movzx | Movsx | Movsxd | Xchg => 0,
        Lea => 1,
        Push => 2,
        Pop => 3,
        Add | Adc | Sub | Sbb | Inc | Dec | Neg => 4,
        Imul | Mul | Idiv | Div => 5,
        And | Or | Xor | Not => 6,
        Shl | Shr | Sar | Rol | Ror | Shld | Shrd => 7,
        Cmp => 8,
        Test => 9,
        Nop | Int3 => 14,
        Leave => 15,
        _ => 16,
    }
}

fn near_target(insn: &Instruction) -> Option<u64> {
    match insn.op0_kind() {
        OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64 => {
            Some(insn.near_branch_target())
        }
        _ => None,
    }
}

fn in_span(t: u64, entry: u64, cap: usize) -> bool {
    t >= entry && (t - entry) as usize <= cap
}

struct Base {
    f: Fingerprint,
    targets: Vec<u64>, // outgoing call + tail-jump destinations
}

/// Cheap, build-invariant summary of a callee — (instructions, calls, branches) over a bounded
/// linear scan from its entry. Enough to tell one thunk's target from another's.
fn light_summary(img: &CodeImage, addr: u64) -> (u32, u32, u32) {
    let Some(code) = img.code_at(addr) else {
        return (0, 0, 0);
    };
    let mut dec = Decoder::with_ip(64, code, addr, DecoderOptions::NONE);
    let (mut ins, mut calls, mut br) = (0u32, 0u32, 0u32);
    while dec.can_decode() && ins < 48 {
        let insn = dec.decode();
        if insn.len() == 0 || insn.is_invalid() {
            break;
        }
        ins += 1;
        match insn.flow_control() {
            FlowControl::Call | FlowControl::IndirectCall => calls += 1,
            FlowControl::ConditionalBranch
            | FlowControl::UnconditionalBranch
            | FlowControl::IndirectBranch => br += 1,
            FlowControl::Return => break,
            _ => {}
        }
    }
    (ins, calls, br)
}

/// Extract the fingerprint of `entry` — base structure plus one hop of call-graph context.
pub fn extract(img: &CodeImage, entry: u64) -> Option<Fingerprint> {
    let base = extract_base(img, entry)?;
    let mut f = base.f;
    let mut seen = HashSet::new();
    for &t in &base.targets {
        if t != entry && img.is_code(t) && seen.insert(t) {
            let (ins, calls, br) = light_summary(img, t);
            f.ctx_ins = f.ctx_ins.wrapping_add(ins);
            f.ctx_calls = f.ctx_calls.wrapping_add(calls);
            f.ctx_br = f.ctx_br.wrapping_add(br);
        }
    }
    Some(f)
}

/// Base structural features + the function's outgoing call/tail-jump targets.
fn extract_base(img: &CodeImage, entry: u64) -> Option<Base> {
    let code = img.code_at(entry)?;
    const MAX_SPAN: usize = 96 * 1024;
    const MAX_INSNS: u32 = 8000;
    let cap = code.len().min(MAX_SPAN);

    let mut f = Fingerprint::default();
    let mut visited: HashSet<u64> = HashSet::new();
    let mut leaders: HashSet<u64> = HashSet::new();
    let mut callees: HashSet<u64> = HashSet::new();
    let mut targets: Vec<u64> = Vec::new();
    let mut hi = entry;
    let mut work = vec![entry];
    leaders.insert(entry);

    while let Some(start) = work.pop() {
        let mut ip = start;
        loop {
            if ip < entry || (ip - entry) as usize >= cap || f.insns >= MAX_INSNS {
                break;
            }
            if !visited.insert(ip) {
                break; // this path merged into already-decoded code
            }
            let off = (ip - entry) as usize;
            let mut dec = Decoder::with_ip(64, &code[off..], ip, DecoderOptions::NONE);
            if !dec.can_decode() {
                break;
            }
            let insn = dec.decode();
            let ilen = insn.len() as u64;
            if ilen == 0 || insn.is_invalid() {
                break;
            }
            f.insns += 1;
            f.cats[category(&insn)] += 1;
            if insn.is_ip_rel_memory_operand() {
                f.data_refs += 1;
                if let Some(bucket) = string_bucket(img, insn.memory_displacement64()) {
                    f.refs[bucket] += 1;
                }
            }
            if ip + ilen > hi {
                hi = ip + ilen;
            }
            let next = ip + ilen;
            match insn.flow_control() {
                FlowControl::Return => {
                    f.rets += 1;
                    break;
                }
                FlowControl::Call => {
                    f.calls += 1;
                    match near_target(&insn) {
                        Some(t) => {
                            callees.insert(t);
                            targets.push(t);
                        }
                        None => f.indirect += 1,
                    }
                    ip = next;
                }
                FlowControl::IndirectCall => {
                    f.calls += 1;
                    f.indirect += 1;
                    ip = next;
                }
                FlowControl::ConditionalBranch => {
                    f.cond_branches += 1;
                    if let Some(t) = near_target(&insn)
                        && in_span(t, entry, cap)
                    {
                        leaders.insert(t);
                        work.push(t);
                    }
                    leaders.insert(next);
                    ip = next;
                }
                FlowControl::UnconditionalBranch => {
                    f.uncond_branches += 1;
                    match near_target(&insn) {
                        Some(t) if in_span(t, entry, cap) => {
                            leaders.insert(t);
                            ip = t;
                        }
                        Some(t) => {
                            targets.push(t); // tail call (thunk target)
                            break;
                        }
                        None => break, // indirect jump
                    }
                }
                FlowControl::IndirectBranch => {
                    f.indirect += 1;
                    break;
                }
                _ => ip = next,
            }
        }
    }

    if f.insns == 0 {
        return None;
    }
    f.blocks = leaders.len() as u32;
    f.distinct_callees = callees.len() as u32;
    f.size_bytes = (hi - entry) as u32;
    Some(Base { f, targets })
}
