use jzon::{array, object, JsonValue};
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::sync::Mutex;

use include_dir::{include_dir, Dir};

use crate::include_file;

static MASTERDATA_JP: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/router/databases/csv");
static MASTERDATA_EN: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/router/databases/csv-en");

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Region {
    Jp,
    En,
}

lazy_static! {
    static ref TABLE_CACHE: Mutex<HashMap<(Region, String), JsonValue>> =
        Mutex::new(HashMap::new());

    // This also needs to be packed into the client - this never changes
    static ref SCHEMAS: JsonValue = jzon::parse(
        &include_file!("src/router/databases/schemas.json")
    ).expect("schemas.json is malformed");
}

fn dir_for(region: Region) -> &'static Dir<'static> {
    match region {
        Region::Jp => &MASTERDATA_JP,
        Region::En => &MASTERDATA_EN,
    }
}

fn region_subdir(region: Region) -> &'static str {
    match region {
        Region::Jp => "csv",
        Region::En => "csv-en",
    }
}

pub fn get_all(region: Region) -> JsonValue {
    let mut rv = object!{};

    for file in dir_for(region).files() {
        let table_name = file.path()
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or_default();

        if table_name.is_empty() {
            continue;
        }

        if let Some(bytes) = csv_bytes(region, table_name) {
            rv[table_name] = String::from_utf8(bytes).unwrap_or_default().into();
        }
    }

    rv
}

pub fn csv_bytes(region: Region, name: &str) -> Option<Vec<u8>> {
    let rel = format!("{}/{}.csv", region_subdir(region), name);
    if let Some(bytes) = crate::runtime::read_masterdata_file(&rel) {
        return Some(bytes);
    }
    dir_for(region)
        .get_file(format!("{name}.csv"))
        .map(|f| f.contents().to_vec())
}

pub fn table(region: Region, name: &str) -> JsonValue {
    let key = (region, name.to_owned());
    if let Some(cached) = TABLE_CACHE.lock().unwrap().get(&key) {
        return cached.clone();
    }

    let bytes = csv_bytes(region, name).unwrap_or_else(|| {
        panic!("masterdata CSV not bundled: {name}.csv ({region:?})")
    });
    let parsed = parse_csv(name, &bytes);

    TABLE_CACHE.lock().unwrap().insert(key, parsed.clone());
    parsed
}

fn field_types(table_name: &str) -> HashMap<String, String> {
    let mut out = HashMap::new();
    let table = &SCHEMAS["tables"][table_name];
    for f in table["fields"].members() {
        out.insert(f["name"].to_string(), f["type"].to_string());
    }
    out
}

fn parse_csv(table_name: &str, bytes: &[u8]) -> JsonValue {
    let types = field_types(table_name);

    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(true)
        .flexible(true)
        .from_reader(bytes);

    let raw_headers: Vec<String> = rdr
        .headers()
        .expect("malformed CSV header")
        .iter()
        .enumerate()
        .map(|(i, h)| {
            if i == 0 {
                h.trim_start_matches('\u{feff}').to_owned()
            } else {
                h.to_owned()
            }
        })
        .collect();

    let json_keys: Vec<String> = raw_headers
        .iter()
        .map(|h| h.strip_prefix('_').unwrap_or(h).to_owned())
        .collect();

    // Default missing schema columns to "string" — safer than guessing.
    let column_types: Vec<&str> = raw_headers
        .iter()
        .map(|h| types.get(h.as_str()).map(String::as_str).unwrap_or("string"))
        .collect();

    let mut out = array![];
    for record in rdr.records() {
        let record = record.expect("malformed CSV row");
        let mut row = object! {};
        for (i, raw) in record.iter().enumerate() {
            let key = match json_keys.get(i) {
                Some(k) => k.as_str(),
                None => continue, // extra trailing columns — ignore
            };
            row[key] = coerce(raw, column_types[i]);
        }
        out.push(row).expect("array push");
    }
    out
}

fn coerce(raw: &str, type_token: &str) -> JsonValue {
    if let Some(elem) = type_token.strip_suffix("[]") {
        if raw.is_empty() {
            return array![];
        }
        let mut out = array![];
        for part in raw.split(',') {
            out.push(coerce(part, elem)).expect("array push");
        }
        return out;
    }

    match type_token {
        "string" => JsonValue::String(raw.to_owned()),
        "bool" => JsonValue::Boolean(matches!(raw, "1" | "true" | "True" | "TRUE")),
        "float" | "double" => {
            if raw.is_empty() {
                JsonValue::from(0.0)
            } else {
                raw.parse::<f64>()
                    .map(JsonValue::from)
                    .unwrap_or_else(|_| JsonValue::String(raw.to_owned()))
            }
        }
        _ => {
            if raw.is_empty() {
                JsonValue::from(0)
            } else {
                raw.parse::<i64>()
                    .map(JsonValue::from)
                    .unwrap_or_else(|_| JsonValue::String(raw.to_owned()))
            }
        }
    }
}
