use std::collections::BTreeMap;

use crate::inspect::smp::catalog::offsets::world::RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT;
use crate::inspect::smp::common::{
    SmpSaveTaggedCollectionHeaderProbe, parse_nonzero_u32, read_ascii_c_string_at, read_f64_at,
    read_u8_at, read_u32_at, read_u64_at, round_f64_to_i64,
};
use crate::inspect::smp::world::*;

pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_NAME_OFFSET: usize = 0x08;
pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN: usize = 0x1f;
pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_CASH_OFFSET: usize = 0x154;
pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET: usize = 0x15d;
pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET: usize = 0x1dd;
pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET: usize = 0x1e9;
pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET: usize = 0x1f1;
pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET: usize = 0x291;
pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERMS_OFFSET: usize = 0x35b;
pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERM_COUNT: usize =
    RT3_SAVE_WORLD_BLOCK_ISSUE_OPINION_TERM_COUNT;
pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS: [usize; 7] =
    [0x1e9, 0x1f1, 0x1f9, 0x201, 0x209, 0x211, 0x219];
pub(in crate::inspect::smp) const SAVE_CHAIRMAN_RECORD_START_SCAN_LIMIT: usize = 0x80;

pub(in crate::inspect::smp) fn build_save_qword_candidate(
    bytes: &[u8],
    record_offset: usize,
    relative_offset: usize,
) -> Option<SmpSaveScalarCandidate> {
    let raw_u64 = read_u64_at(bytes, record_offset + relative_offset)?;
    Some(SmpSaveScalarCandidate {
        relative_offset,
        relative_offset_hex: format!("0x{relative_offset:x}"),
        raw_u64,
        raw_u64_hex: format!("0x{raw_u64:016x}"),
        value_i64: raw_u64 as i64,
        value_f64: f64::from_bits(raw_u64),
    })
}

pub(in crate::inspect::smp) fn derive_chairman_holdings_share_price_total(
    holdings_by_company: &BTreeMap<u32, u32>,
    company_share_prices: &BTreeMap<u32, i64>,
) -> Option<i64> {
    let mut total = 0i64;
    for (company_id, units) in holdings_by_company {
        let share_price = *company_share_prices.get(company_id)?;
        total = total.checked_add((*units as i64).checked_mul(share_price)?)?;
    }
    Some(total)
}

pub(in crate::inspect::smp) fn derive_chairman_cached_purchasing_power_total(
    current_cash: i64,
    cached_scalar_candidates: &[SmpSaveScalarCandidate],
) -> Option<i64> {
    let strongest_cached_total = cached_scalar_candidates
        .iter()
        .filter_map(|candidate| round_f64_to_i64(candidate.value_f64))
        .filter(|value| *value >= 0)
        .max()?;
    current_cash.checked_add(strongest_cached_total)
}

pub(in crate::inspect::smp) fn parse_save_chairman_profile_table_probe(
    bytes: &[u8],
    header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
    selection_probe: Option<&SmpSaveWorldSelectionContextProbe>,
    company_header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
) -> Option<SmpLoadedChairmanProfileTable> {
    let header_probe = header_probe?;
    let observed_entry_count = header_probe.live_record_count as usize;
    if observed_entry_count == 0 {
        return Some(SmpLoadedChairmanProfileTable {
            source_kind: "save-chairman-profile-direct-records".to_string(),
            semantic_family: "scenario-save-chairman-profile-direct-records".to_string(),
            observed_entry_count,
            selected_chairman_profile_id: selection_probe
                .map(|probe| probe.selected_chairman_profile_id),
            entries: Vec::new(),
        });
    }

    let record_start_offset =
        detect_save_chairman_profile_record_start_offset(bytes, header_probe)?;
    let record_stride = header_probe.direct_record_stride as usize;
    let base_offset = header_probe
        .metadata_tag_offset
        .checked_add(4)?
        .checked_add(record_start_offset)?;
    let company_id_bound = company_header_probe
        .map(|probe| probe.live_id_bound)
        .unwrap_or(0);

    let mut entries = Vec::with_capacity(observed_entry_count);
    for index in 0usize..observed_entry_count {
        let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?;
        let profile_id = read_u32_at(bytes, record_offset)?;
        let active = read_u32_at(bytes, record_offset + 4)? != 0;
        let name = read_ascii_c_string_at(
            bytes,
            record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET,
            SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN,
        )?;
        let current_cash = round_f64_to_i64(read_f64_at(
            bytes,
            record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET,
        )?)?;
        let linked_company_id = parse_nonzero_u32(
            bytes,
            record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET,
        )?;
        let personality_byte_0x291 = read_u8_at(
            bytes,
            record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET,
        )?;
        let cache_0 = round_f64_to_i64(read_f64_at(
            bytes,
            record_offset + SAVE_CHAIRMAN_RECORD_CACHE_0_OFFSET,
        )?)?;
        let cache_1 = round_f64_to_i64(read_f64_at(
            bytes,
            record_offset + SAVE_CHAIRMAN_RECORD_CACHE_1_OFFSET,
        )?)?;
        let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS
            .iter()
            .map(|relative_offset| {
                build_save_qword_candidate(bytes, record_offset, *relative_offset)
            })
            .collect::<Option<Vec<_>>>()?;
        let issue_opinion_terms_raw_i32 = build_save_i32_term_strip(
            bytes,
            record_offset,
            SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERMS_OFFSET,
            SAVE_CHAIRMAN_RECORD_ISSUE_OPINION_TERM_COUNT,
        )?;
        let holdings_value_total = cache_0.max(cache_1).max(0);
        let net_worth_total = current_cash.saturating_add(holdings_value_total);
        let purchasing_power_total =
            derive_chairman_cached_purchasing_power_total(current_cash, &cached_scalar_candidates)
                .unwrap_or(net_worth_total);
        let mut company_holdings = BTreeMap::new();
        for company_id in 1..=company_id_bound {
            let slot_offset = record_offset
                .checked_add(SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET)?
                .checked_add((company_id as usize).checked_mul(4)?)?;
            let units = read_u32_at(bytes, slot_offset)?;
            if units != 0 {
                company_holdings.insert(company_id, units);
            }
        }
        entries.push(SmpLoadedChairmanProfileEntry {
            profile_id,
            name,
            active,
            current_cash,
            linked_company_id,
            company_holdings,
            holdings_value_total,
            net_worth_total,
            purchasing_power_total,
            personality_byte_0x291: Some(personality_byte_0x291),
            issue_opinion_terms_raw_i32,
        });
    }

    Some(SmpLoadedChairmanProfileTable {
        source_kind: "save-chairman-profile-direct-records".to_string(),
        semantic_family: "scenario-save-chairman-profile-direct-records".to_string(),
        observed_entry_count,
        selected_chairman_profile_id: selection_probe
            .map(|probe| probe.selected_chairman_profile_id),
        entries,
    })
}

pub(in crate::inspect::smp) fn detect_save_chairman_profile_record_start_offset(
    bytes: &[u8],
    header_probe: &SmpSaveTaggedCollectionHeaderProbe,
) -> Option<usize> {
    let observed_entry_count = header_probe.live_record_count as usize;
    let record_stride = header_probe.direct_record_stride as usize;
    let scan_limit = SAVE_CHAIRMAN_RECORD_START_SCAN_LIMIT.min(record_stride);
    let base_offset = header_probe.metadata_tag_offset.checked_add(4)?;
    let mut best_start = None;
    let mut best_score = 0usize;

    for start in 0..scan_limit {
        let mut score = 0usize;
        let mut seen_ids = std::collections::BTreeSet::new();
        let mut valid = true;
        for index in 0usize..observed_entry_count {
            let record_offset = match base_offset
                .checked_add(start)
                .and_then(|offset| offset.checked_add(index.checked_mul(record_stride)?))
            {
                Some(offset) => offset,
                None => {
                    valid = false;
                    break;
                }
            };
            let profile_id = match read_u32_at(bytes, record_offset) {
                Some(value)
                    if value >= 1
                        && value <= header_probe.live_id_bound
                        && seen_ids.insert(value) =>
                {
                    value
                }
                _ => {
                    valid = false;
                    break;
                }
            };
            match read_u32_at(bytes, record_offset + 4) {
                Some(0 | 1) => score += 1,
                _ => {
                    valid = false;
                    break;
                }
            }
            let name = match read_ascii_c_string_at(
                bytes,
                record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET,
                SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN,
            ) {
                Some(name) if !name.is_empty() && name.chars().all(is_save_name_char) => name,
                _ => {
                    valid = false;
                    break;
                }
            };
            match read_u32_at(
                bytes,
                record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET,
            ) {
                Some(value) if value <= 0x100 => score += (value != 0) as usize,
                _ => {
                    valid = false;
                    break;
                }
            }
            match read_f64_at(bytes, record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET) {
                Some(value) if value.is_finite() && value.abs() < 1.0e12 => {
                    score += name.len() + 4;
                }
                _ => {
                    valid = false;
                    break;
                }
            }
            if profile_id == (index + 1) as u32 {
                score += 4;
            }
        }
        if valid && score > best_score {
            best_score = score;
            best_start = Some(start);
        }
    }

    best_start
}

pub(in crate::inspect::smp) fn is_save_name_char(ch: char) -> bool {
    ch.is_ascii_alphanumeric()
        || matches!(
            ch,
            ' ' | '&' | '\'' | ',' | '.' | '-' | '/' | '(' | ')' | ':'
        )
}

pub(in crate::inspect::smp) fn build_save_chairman_record_analysis_entries(
    bytes: &[u8],
    header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
    company_header_probe: Option<&SmpSaveTaggedCollectionHeaderProbe>,
    company_share_prices: &BTreeMap<u32, i64>,
) -> Option<Vec<SmpSaveChairmanRecordAnalysisEntry>> {
    let Some(header_probe) = header_probe else {
        return Some(Vec::new());
    };
    let record_start_offset =
        detect_save_chairman_profile_record_start_offset(bytes, header_probe)?;
    let record_stride = header_probe.direct_record_stride as usize;
    let base_offset = header_probe
        .metadata_tag_offset
        .checked_add(4)?
        .checked_add(record_start_offset)?;
    let company_id_bound = company_header_probe
        .map(|probe| probe.live_id_bound)
        .unwrap_or(0);
    let mut entries = Vec::with_capacity(header_probe.live_record_count as usize);
    for index in 0usize..header_probe.live_record_count as usize {
        let record_offset = base_offset.checked_add(index.checked_mul(record_stride)?)?;
        let profile_id = read_u32_at(bytes, record_offset)?;
        let active = read_u32_at(bytes, record_offset + 4)? != 0;
        let name = read_ascii_c_string_at(
            bytes,
            record_offset + SAVE_CHAIRMAN_RECORD_NAME_OFFSET,
            SAVE_CHAIRMAN_RECORD_NAME_MAX_LEN,
        )?;
        let current_cash = read_f64_at(bytes, record_offset + SAVE_CHAIRMAN_RECORD_CASH_OFFSET)?;
        let linked_company_id = parse_nonzero_u32(
            bytes,
            record_offset + SAVE_CHAIRMAN_RECORD_LINKED_COMPANY_OFFSET,
        )?;
        let personality_byte_0x291 = read_u8_at(
            bytes,
            record_offset + SAVE_CHAIRMAN_RECORD_PERSONALITY_BYTE_0X291_OFFSET,
        )?;
        let mut holdings_by_company = BTreeMap::new();
        for company_id in 1..=company_id_bound {
            let slot_offset = record_offset
                .checked_add(SAVE_CHAIRMAN_RECORD_HOLDINGS_BASE_OFFSET)?
                .checked_add((company_id as usize).checked_mul(4)?)?;
            let units = read_u32_at(bytes, slot_offset)?;
            if units != 0 {
                holdings_by_company.insert(company_id, units);
            }
        }
        let cached_scalar_candidates = SAVE_CHAIRMAN_RECORD_CACHE_CANDIDATE_OFFSETS
            .iter()
            .map(|relative_offset| {
                build_save_qword_candidate(bytes, record_offset, *relative_offset)
            })
            .collect::<Option<Vec<_>>>()?;
        let rounded_current_cash = round_f64_to_i64(current_cash)?;
        let derived_holdings_share_price_total =
            derive_chairman_holdings_share_price_total(&holdings_by_company, company_share_prices);
        let derived_net_worth_share_price_total = derived_holdings_share_price_total
            .and_then(|holdings_total| rounded_current_cash.checked_add(holdings_total));
        let derived_cached_purchasing_power_total = derive_chairman_cached_purchasing_power_total(
            rounded_current_cash,
            &cached_scalar_candidates,
        );
        entries.push(SmpSaveChairmanRecordAnalysisEntry {
            profile_id,
            name,
            active,
            current_cash,
            linked_company_id,
            holdings_by_company,
            derived_holdings_share_price_total,
            derived_net_worth_share_price_total,
            derived_cached_purchasing_power_total,
            personality_byte_0x291,
            personality_byte_0x291_hex: format!("0x{personality_byte_0x291:02x}"),
            cached_scalar_candidates,
        });
    }
    Some(entries)
}
