use serde::Serialize;
use std::collections::HashMap;

// Game data is embedded at compile time. These files come from the original
// Lailloken/Exile-UI data set (PoE2 variants).
const AREAS_JSON: &str = include_str!("../data/areas2.json");
const GUIDE_JSON: &str = include_str!("../data/guide2.json");
const GEMS_JSON: &str = include_str!("../data/gems2.json");

#[derive(Serialize, Clone, Debug)]
pub struct Area {
    pub id: String,
    pub name: String,
    /// Recommended character level when entering this area (raw "min | max" string).
    pub recommendation: Option<String>,
    /// Index of the area group this belongs to (acts: 0..n).
    pub group: usize,
}

#[derive(Serialize, Clone, Debug)]
pub struct Step {
    /// Lines of the step, in the original markup language.
    pub lines: Vec<String>,
    /// Guide section index (roughly corresponds to an act).
    pub section: usize,
    /// Area the player should travel to during this step, if detectable.
    pub target_area: Option<String>,
}

#[derive(Serialize, Clone)]
pub struct GuideData {
    pub steps: Vec<Step>,
    /// areaID -> Area metadata.
    pub areas: HashMap<String, Area>,
    /// Ordered list of area-group "names" (first travelable area id of each group) for labels.
    pub section_count: usize,
}

/// Resolve the area an `areaid…` token points at within a step.
fn extract_target_area(lines: &[String]) -> Option<String> {
    let mut last: Option<String> = None;
    let mut preferred: Option<String> = None;
    for line in lines {
        let lower = line.to_lowercase();
        let is_travel = lower.contains("enter ")
            || lower.contains("waypoint")
            || lower.contains("portal")
            || lower.contains("to areaid");
        let mut search = line.as_str();
        while let Some(pos) = search.find("areaid") {
            let rest = &search[pos + "areaid".len()..];
            let id: String = rest
                .chars()
                .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
                .collect();
            if !id.is_empty() {
                last = Some(id.clone());
                if is_travel {
                    preferred = Some(id);
                }
            }
            // advance past this token
            let consumed = pos + "areaid".len() + rest.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_').count();
            if consumed >= search.len() {
                break;
            }
            search = &search[consumed..];
        }
    }
    preferred.or(last)
}

/// Normalize a guide entry into (lines, optional condition (key, value)).
fn parse_entry(entry: &serde_json::Value) -> (Vec<String>, Option<(String, String)>) {
    if let Some(arr) = entry.as_array() {
        let lines = arr
            .iter()
            .filter_map(|v| v.as_str().map(|s| s.to_string()))
            .collect();
        (lines, None)
    } else if let Some(obj) = entry.as_object() {
        let lines = obj
            .get("lines")
            .and_then(|v| v.as_array())
            .map(|a| {
                a.iter()
                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
                    .collect()
            })
            .unwrap_or_default();
        let condition = obj.get("condition").and_then(|v| v.as_array()).and_then(|a| {
            match (a.first().and_then(|x| x.as_str()), a.get(1).and_then(|x| x.as_str())) {
                (Some(k), Some(v)) => Some((k.to_string(), v.to_string())),
                _ => None,
            }
        });
        (lines, condition)
    } else {
        (Vec::new(), None)
    }
}

pub fn load(league_start: bool) -> GuideData {
    // --- areas ---
    let mut areas: HashMap<String, Area> = HashMap::new();
    if let Ok(groups) = serde_json::from_str::<Vec<Vec<serde_json::Value>>>(AREAS_JSON) {
        for (gi, group) in groups.iter().enumerate() {
            for a in group {
                let id = a.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
                if id.is_empty() {
                    continue;
                }
                let name = a.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
                let recommendation = a
                    .get("recommendation")
                    .and_then(|v| v.as_str())
                    .map(|s| s.to_string());
                areas.insert(
                    id.clone(),
                    Area {
                        id,
                        name,
                        recommendation,
                        group: gi,
                    },
                );
            }
        }
    }

    // --- guide ---
    // Structure: [ section[ entry, ... ], ... ] where each entry is either an
    // array of line strings, or an object { "condition": [key, value], "lines": [...] }.
    // Conditional entries currently only encode "league-start" branches.
    let mut steps: Vec<Step> = Vec::new();
    let mut section_count = 0;
    if let Ok(sections) = serde_json::from_str::<Vec<Vec<serde_json::Value>>>(GUIDE_JSON) {
        section_count = sections.len();
        for (si, section) in sections.into_iter().enumerate() {
            for entry in section {
                let (lines, condition) = parse_entry(&entry);
                // Skip the branch that doesn't apply to the chosen play mode.
                if let Some((key, val)) = &condition {
                    if key == "league-start" {
                        let wants_yes = val == "yes";
                        if wants_yes != league_start {
                            continue;
                        }
                    }
                }
                if lines.is_empty() {
                    continue;
                }
                let target_area = extract_target_area(&lines);
                steps.push(Step {
                    lines,
                    section: si,
                    target_area,
                });
            }
        }
    }

    GuideData {
        steps,
        areas,
        section_count,
    }
}

/// Raw gems data (skill/spirit/support -> name -> [level, quality-ish]) passed to the UI as-is.
pub fn gems_raw() -> serde_json::Value {
    serde_json::from_str(GEMS_JSON).unwrap_or(serde_json::Value::Null)
}

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

    #[test]
    fn extracts_travel_target() {
        let lines = vec![
            "kill some_boss".to_string(),
            "(img:portal) to areaidg1_town ;; clearfell encampment".to_string(),
        ];
        assert_eq!(extract_target_area(&lines).as_deref(), Some("g1_town"));
    }

    #[test]
    fn prefers_travel_line_over_mention() {
        let lines = vec![
            "optional: boss in areaidg1_3".to_string(),
            "enter areaidg1_4 ;; the grelwood".to_string(),
        ];
        assert_eq!(extract_target_area(&lines).as_deref(), Some("g1_4"));
    }

    #[test]
    fn guide_and_areas_load() {
        let g = load(true);
        assert!(!g.steps.is_empty(), "guide steps should load");
        assert!(g.areas.contains_key("g1_1"), "areas should contain g1_1");
        assert!(g.section_count >= 6);
    }
}

/// Find the step index whose target area equals `area_id`, preferring the match
/// closest to `current`. Returns None if no step targets that area.
pub fn step_for_area(steps: &[Step], area_id: &str, current: usize) -> Option<usize> {
    let mut best: Option<usize> = None;
    let mut best_dist = usize::MAX;
    for (i, step) in steps.iter().enumerate() {
        if let Some(t) = &step.target_area {
            if t == area_id {
                let dist = if i >= current { i - current } else { current - i };
                if dist < best_dist {
                    best_dist = dist;
                    best = Some(i);
                }
            }
        }
    }
    best
}
