use std::collections::BTreeMap;

use super::CompanyProjection;
use crate::inspect::smp::save_load::SmpLoadedSaveSlice;
use crate::inspect::smp::structures::SmpLoadedPlacedStructureEntry;
use crate::state::{
    RuntimeNearCityAcquisitionRegion, RuntimeNearCityAcquisitionSite,
    RuntimeNearCityAcquisitionValueProvenance,
};

pub(super) struct NearCityProjection {
    pub(super) regions: Vec<RuntimeNearCityAcquisitionRegion>,
    pub(super) sites: Vec<RuntimeNearCityAcquisitionSite>,
    pub(super) has_projection: bool,
}

pub(super) fn project_near_city_projection(
    save_slice: &SmpLoadedSaveSlice,
    company_projection: &CompanyProjection,
    metadata: &mut BTreeMap<String, String>,
) -> NearCityProjection {
    let regions = project_near_city_regions(save_slice);
    let preferred_company_id = preferred_near_city_company_id(company_projection);
    let sites = project_near_city_sites(save_slice, &regions, preferred_company_id);
    if !regions.is_empty() {
        metadata.insert(
            "save_slice.near_city_region_projection_count".to_string(),
            regions.len().to_string(),
        );
    }
    if !sites.is_empty() {
        metadata.insert(
            "save_slice.near_city_site_projection_count".to_string(),
            sites.len().to_string(),
        );
    }

    NearCityProjection {
        has_projection: !regions.is_empty() || !sites.is_empty(),
        regions,
        sites,
    }
}

fn project_near_city_regions(
    save_slice: &SmpLoadedSaveSlice,
) -> Vec<RuntimeNearCityAcquisitionRegion> {
    let best_candidate = save_slice
        .region_fixed_row_run_summary
        .as_ref()
        .and_then(|summary| summary.candidates.first());
    save_slice
        .region_collection
        .as_ref()
        .map(|collection| {
            collection
                .entries
                .iter()
                .map(|entry| RuntimeNearCityAcquisitionRegion {
                    region_id: entry.record_index as u32,
                    name: entry.name.clone(),
                    profile_names: entry
                        .profile_collection
                        .as_ref()
                        .map(|collection| {
                            collection
                                .entries
                                .iter()
                                .map(|profile| profile.name.clone())
                                .collect()
                        })
                        .unwrap_or_default(),
                    fixed_row_shape_family_signature: best_candidate
                        .map(|candidate| candidate.shape_family_signature.clone()),
                    fixed_row_best_density_lane_relative_offset_hex: best_candidate.and_then(
                        |candidate| {
                            candidate
                                .best_probable_density_lane_relative_offset_hex
                                .clone()
                        },
                    ),
                })
                .collect()
        })
        .unwrap_or_default()
}

fn project_near_city_sites(
    save_slice: &SmpLoadedSaveSlice,
    regions: &[RuntimeNearCityAcquisitionRegion],
    preferred_company_id: Option<u32>,
) -> Vec<RuntimeNearCityAcquisitionSite> {
    let side_buffer = save_slice
        .placed_structure_dynamic_side_buffer_summary
        .as_ref();
    let fallback_lane_0 = side_buffer
        .and_then(|summary| parse_hex_u32(&summary.owner_shared_dword_hex))
        .unwrap_or_default();
    let fallback_lane_1 = save_slice
        .region_fixed_row_run_summary
        .as_ref()
        .and_then(|summary| summary.candidates.first())
        .map(|candidate| candidate.row_stride as u32)
        .unwrap_or_default();
    let fallback_lane_2 = save_slice
        .region_fixed_row_run_summary
        .as_ref()
        .and_then(|summary| summary.candidates.first())
        .and_then(|candidate| {
            candidate
                .best_probable_density_lane_relative_offset_hex
                .as_ref()
                .and_then(|hex| parse_hex_u32(hex))
        })
        .unwrap_or_default();

    save_slice
        .placed_structure_collection
        .as_ref()
        .map(|collection| {
            collection
                .entries
                .iter()
                .map(|entry| {
                    let region_id = match_near_city_region_id(entry, regions);
                    let subtype_label = classify_near_city_site_subtype(entry);
                    let industrial_like = matches!(
                        subtype_label.as_str(),
                        "farm" | "mine" | "industry" | "industry_like"
                    );
                    let matched_pair = side_buffer.and_then(|summary| {
                        summary.name_pair_summaries.iter().find(|pair| {
                            normalize_name(&pair.primary_name)
                                == normalize_name(&entry.primary_name)
                                && normalize_name(&pair.secondary_name)
                                    == normalize_name(&entry.secondary_name)
                        })
                    });
                    let cached_tri_lane_0 = matched_pair
                        .map(|pair| pair.dominant_prefix_leading_dword)
                        .unwrap_or(fallback_lane_0);
                    let cached_tri_lane_1 = matched_pair
                        .map(|pair| pair.dominant_prefix_trailing_word as u32)
                        .unwrap_or(fallback_lane_1);
                    let cached_tri_lane_2 = if fallback_lane_2 != 0 {
                        fallback_lane_2
                    } else {
                        entry.profile_payload_dword
                    };
                    let cached_tri_lane_provenance =
                        if matched_pair.is_some() && fallback_lane_2 != 0 {
                            RuntimeNearCityAcquisitionValueProvenance::Grounded
                        } else {
                            RuntimeNearCityAcquisitionValueProvenance::BestEffortGuess
                        };
                    let owner_company_id = if industrial_like {
                        preferred_company_id
                    } else {
                        None
                    };

                    RuntimeNearCityAcquisitionSite {
                        site_id: entry.record_index as u32,
                        primary_name: entry.primary_name.clone(),
                        secondary_name: entry.secondary_name.clone(),
                        region_id,
                        owner_company_id,
                        preferred_company_id,
                        owner_company_id_provenance: if owner_company_id.is_some() {
                            RuntimeNearCityAcquisitionValueProvenance::BestEffortGuess
                        } else {
                            RuntimeNearCityAcquisitionValueProvenance::Grounded
                        },
                        self_id: entry.record_index as u32,
                        self_id_provenance: RuntimeNearCityAcquisitionValueProvenance::Grounded,
                        candidate_subtype_label: subtype_label,
                        candidate_subtype_provenance:
                            RuntimeNearCityAcquisitionValueProvenance::Grounded,
                        cached_tri_lane_0,
                        cached_tri_lane_1,
                        cached_tri_lane_2,
                        cached_tri_lane_provenance,
                        nontransport_source_label: if owner_company_id.is_some() {
                            "best_effort_company_market_latch_projection".to_string()
                        } else {
                            "transport_or_unknown_subtype".to_string()
                        },
                        tri_lane_source_label: if matched_pair.is_some() && fallback_lane_2 != 0 {
                            "matched_side_buffer_name_pair_plus_region_fixed_row_candidate"
                                .to_string()
                        } else if matched_pair.is_some() {
                            "matched_side_buffer_name_pair_plus_profile_payload_fallback"
                                .to_string()
                        } else {
                            "owner_shared_dword_and_region_row_fallback".to_string()
                        },
                    }
                })
                .collect()
        })
        .unwrap_or_default()
}

fn preferred_near_city_company_id(company_projection: &CompanyProjection) -> Option<u32> {
    let selected_company_id = company_projection.selected_company_id;
    if selected_company_id.is_some_and(|company_id| {
        company_projection
            .companies
            .iter()
            .any(|company| company.company_id == company_id && company.active)
    }) {
        return selected_company_id;
    }

    company_projection
        .companies
        .iter()
        .filter(|company| company.active)
        .max_by_key(|company| {
            company_projection
                .company_market_state
                .get(&company.company_id)
                .map(|market_state| {
                    let mut score = 0_u8;
                    if market_state.linked_transit_latch {
                        score += 4;
                    }
                    if market_state.city_connection_latch {
                        score += 2;
                    }
                    if market_state.linked_transit_route_anchor_entry_id.is_some() {
                        score += 1;
                    }
                    score
                })
                .unwrap_or_default()
        })
        .map(|company| company.company_id)
}

fn match_near_city_region_id(
    entry: &SmpLoadedPlacedStructureEntry,
    regions: &[RuntimeNearCityAcquisitionRegion],
) -> Option<u32> {
    let primary_name = normalize_name(&entry.primary_name);
    let secondary_name = normalize_name(&entry.secondary_name);
    regions
        .iter()
        .find(|region| {
            normalize_name(&region.name) == primary_name
                || region
                    .profile_names
                    .iter()
                    .any(|name| normalize_name(name) == primary_name)
                || region
                    .profile_names
                    .iter()
                    .any(|name| normalize_name(name) == secondary_name)
        })
        .map(|region| region.region_id)
        .or_else(|| regions.first().map(|region| region.region_id))
}

fn classify_near_city_site_subtype(entry: &SmpLoadedPlacedStructureEntry) -> String {
    let primary = normalize_name(&entry.primary_name);
    let secondary = normalize_name(&entry.secondary_name);
    let combined = format!("{primary} {secondary}");
    if combined.contains("farm") {
        "farm".to_string()
    } else if combined.contains("mine") {
        "mine".to_string()
    } else if combined.contains("factory")
        || combined.contains("plant")
        || combined.contains("mill")
        || combined.contains("works")
        || combined.contains("auto")
        || combined.contains("refinery")
    {
        "industry".to_string()
    } else if combined.contains("station")
        || combined.contains("depot")
        || combined.contains("port")
        || combined.contains("warehouse")
    {
        "transport".to_string()
    } else if entry.profile_status_kind.contains("farm") {
        "farm".to_string()
    } else if entry.profile_payload_dword == 0x00005dc1
        || entry.profile_companion_byte_u8 == Some(7)
    {
        "industry_like".to_string()
    } else {
        "unknown".to_string()
    }
}

fn normalize_name(name: &str) -> String {
    name.chars()
        .filter(|ch| ch.is_ascii_alphanumeric())
        .flat_map(|ch| ch.to_lowercase())
        .collect()
}

fn parse_hex_u32(text: &str) -> Option<u32> {
    u32::from_str_radix(text.trim_start_matches("0x"), 16).ok()
}
