use crate::{
    articulation_points_4d, bridges_4d, earliest_arrival_path_4d, reachable_4d,
    time_dependent_dijkstra_4d, GraphNode4D, GraphPath4D, SpatialRegion, TemporalDijkstraResult4D,
    TemporalEdge, TemporalJourney4D, TemporalWindow, TraversalContext4D,
};
use glam::Vec3;
use serde_json::{json, Value};
use std::collections::BTreeMap;

pub type GeoCypherRow = BTreeMap<String, Value>;

#[derive(Debug, Clone, PartialEq)]
pub enum GeoCypherResult {
    NodeIds(Vec<u64>),
    Edges(Vec<(u64, u64)>),
    Rows(Vec<GeoCypherRow>),
    Path(GraphPath4D),
    Journey(TemporalJourney4D),
    Dijkstra(TemporalDijkstraResult4D),
    Bottlenecks {
        articulation_points: Vec<u64>,
        bridges: Vec<(u64, u64)>,
    },
    Components(Vec<Vec<u64>>),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeoCypherError {
    pub message: String,
}

impl GeoCypherError {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }
}

impl std::fmt::Display for GeoCypherError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for GeoCypherError {}

pub fn query_4d(nodes: &[GraphNode4D], query: &str) -> Result<GeoCypherResult, GeoCypherError> {
    let normalized = normalize_query(query);
    let upper = normalized.to_ascii_uppercase();

    if upper.starts_with("MATCH ") {
        return execute_match(nodes, &normalized);
    }
    if upper.starts_with("CALL DB.ROUTE.TEMPORAL") {
        return execute_temporal_route(nodes, &normalized);
    }
    if upper.starts_with("CALL DB.IMPACT.RADIUS") {
        return execute_impact_radius(nodes, &normalized);
    }
    if upper.starts_with("CALL DB.RESILIENCE.BOTTLENECKS") {
        return execute_bottlenecks(nodes, &normalized);
    }
    if upper.starts_with("CALL DB.SIGNAL.PROPAGATE") {
        return execute_signal_propagation(nodes, &normalized);
    }

    Err(GeoCypherError::new("unsupported 4D Cypher query"))
}

fn execute_bottlenecks(
    nodes: &[GraphNode4D],
    query: &str,
) -> Result<GeoCypherResult, GeoCypherError> {
    let args = between(query, "(", ")")?;
    let parts = split_top_level(args, ',');
    if parts.len() != 2 {
        return Err(GeoCypherError::new(
            "db.resilience.bottlenecks expects spatial_sphere and time_window",
        ));
    }

    let spatial_region = parse_spatial_sphere(parts[0])?;
    let time_window = parse_time_window(parts[1])?;
    let ctx = TraversalContext4D {
        time_window: Some(time_window),
        spatial_region: Some(spatial_region),
        ..TraversalContext4D::default()
    };

    Ok(GeoCypherResult::Bottlenecks {
        articulation_points: articulation_points_4d(nodes, &ctx),
        bridges: bridges_4d(nodes, &ctx),
    })
}

fn execute_signal_propagation(
    nodes: &[GraphNode4D],
    query: &str,
) -> Result<GeoCypherResult, GeoCypherError> {
    let args = between(query, "(", ")")?;
    let parts = split_top_level(args, ',');
    if parts.len() < 2 {
        return Err(GeoCypherError::new(
            "db.signal.propagate expects start and departure",
        ));
    }

    let start_id = parse_u64(parts[0])?;
    let departure = parse_named_u64(parts[1], "departure")?;
    let spatial_region = if parts.len() >= 3 {
        Some(parse_spatial_sphere(parts[2])?)
    } else {
        None
    };
    let result = time_dependent_dijkstra_4d(nodes, start_id, departure, spatial_region)
        .ok_or_else(|| GeoCypherError::new("signal propagation start node is not usable"))?;

    Ok(GeoCypherResult::Dijkstra(result))
}

fn execute_match(nodes: &[GraphNode4D], query: &str) -> Result<GeoCypherResult, GeoCypherError> {
    let ctx = parse_context(query)?;
    let return_items = parse_return_items(query)?;
    if return_items
        .iter()
        .any(|item| matches!(item, ReturnItem::Property { .. }))
    {
        return project_match_rows(nodes, &ctx, &return_items).map(GeoCypherResult::Rows);
    }

    Ok(GeoCypherResult::Edges(active_edges(nodes, &ctx)))
}

fn execute_temporal_route(
    nodes: &[GraphNode4D],
    query: &str,
) -> Result<GeoCypherResult, GeoCypherError> {
    let args = between(query, "(", ")")?;
    let parts = split_top_level(args, ',');
    if parts.len() < 3 {
        return Err(GeoCypherError::new(
            "db.route.temporal expects start, goal, and departure",
        ));
    }

    let start_id = parse_u64(parts[0])?;
    let goal_id = parse_u64(parts[1])?;
    let departure = parse_named_u64(parts[2], "departure")?;
    let journey = earliest_arrival_path_4d(nodes, start_id, goal_id, departure, None)
        .ok_or_else(|| GeoCypherError::new("no temporal route found"))?;

    Ok(GeoCypherResult::Journey(journey))
}

fn execute_impact_radius(
    nodes: &[GraphNode4D],
    query: &str,
) -> Result<GeoCypherResult, GeoCypherError> {
    let args = between(query, "(", ")")?;
    let parts = split_top_level(args, ',');
    if parts.len() != 3 {
        return Err(GeoCypherError::new(
            "db.impact.radius expects start, spatial_sphere, and time_window",
        ));
    }

    let start_id = parse_u64(parts[0])?;
    let spatial_region = parse_spatial_sphere(parts[1])?;
    let time_window = parse_time_window(parts[2])?;
    let ctx = TraversalContext4D {
        time_window: Some(time_window),
        spatial_region: Some(spatial_region),
        ..TraversalContext4D::default()
    };

    Ok(GeoCypherResult::NodeIds(reachable_4d(
        nodes, start_id, &ctx,
    )))
}

fn active_edges(nodes: &[GraphNode4D], ctx: &TraversalContext4D) -> Vec<(u64, u64)> {
    let node_map: std::collections::HashMap<u64, &GraphNode4D> =
        nodes.iter().map(|node| (node.id, node)).collect();
    let mut edges = Vec::new();

    for node in nodes {
        if !node_is_valid(node, ctx) {
            continue;
        }
        for edge in &node.successors {
            let Some(next) = node_map.get(&edge.dst).copied() else {
                continue;
            };
            if edge_is_valid(*edge, ctx) && node_is_valid(next, ctx) {
                edges.push((node.id, edge.dst));
            }
        }
    }

    edges.sort_unstable();
    edges
}

fn project_match_rows(
    nodes: &[GraphNode4D],
    ctx: &TraversalContext4D,
    return_items: &[ReturnItem],
) -> Result<Vec<GeoCypherRow>, GeoCypherError> {
    let node_map: std::collections::HashMap<u64, &GraphNode4D> =
        nodes.iter().map(|node| (node.id, node)).collect();
    let mut rows = Vec::new();

    for node in nodes {
        if !node_is_valid(node, ctx) {
            continue;
        }
        for edge in &node.successors {
            let Some(next) = node_map.get(&edge.dst).copied() else {
                continue;
            };
            if !edge_is_valid(*edge, ctx) || !node_is_valid(next, ctx) {
                continue;
            }

            let mut row = GeoCypherRow::new();
            for item in return_items {
                match item {
                    ReturnItem::Alias(alias) => {
                        row.insert(alias.clone(), node_object(alias, node, next)?);
                    }
                    ReturnItem::Property { alias, property } => {
                        let source = match alias.as_str() {
                            "a" => node,
                            "b" => next,
                            _ => {
                                return Err(GeoCypherError::new(format!(
                                    "unknown MATCH alias {alias}"
                                )));
                            }
                        };
                        row.insert(
                            format!("{alias}.{property}"),
                            property_value(source, property),
                        );
                    }
                }
            }
            rows.push(row);
        }
    }

    rows.sort_by(|left, right| format!("{left:?}").cmp(&format!("{right:?}")));
    Ok(rows)
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum ReturnItem {
    Alias(String),
    Property { alias: String, property: String },
}

fn parse_return_items(query: &str) -> Result<Vec<ReturnItem>, GeoCypherError> {
    let upper = query.to_ascii_uppercase();
    let Some(return_idx) = upper.find(" RETURN ") else {
        return Ok(vec![
            ReturnItem::Alias("a".to_string()),
            ReturnItem::Alias("b".to_string()),
        ]);
    };
    let return_clause = &query[return_idx + " RETURN ".len()..];
    let items = split_top_level(return_clause, ',')
        .into_iter()
        .map(parse_return_item)
        .collect::<Result<Vec<_>, _>>()?;

    if items.is_empty() {
        return Err(GeoCypherError::new("RETURN clause is empty"));
    }
    Ok(items)
}

fn parse_return_item(input: &str) -> Result<ReturnItem, GeoCypherError> {
    let item = input.trim();
    if item.is_empty() {
        return Err(GeoCypherError::new("empty RETURN item"));
    }
    if let Some((alias, property)) = item.split_once('.') {
        return Ok(ReturnItem::Property {
            alias: alias.trim().to_string(),
            property: property.trim().to_string(),
        });
    }
    Ok(ReturnItem::Alias(item.to_string()))
}

fn node_object(alias: &str, a: &GraphNode4D, b: &GraphNode4D) -> Result<Value, GeoCypherError> {
    let node = match alias {
        "a" => a,
        "b" => b,
        _ => return Err(GeoCypherError::new(format!("unknown MATCH alias {alias}"))),
    };

    Ok(json!({
        "id": node.id,
        "x": node.x,
        "y": node.y,
        "z": node.z,
        "begin_ts": node.begin_ts,
        "end_ts": node.end_ts,
        "properties": node.properties,
    }))
}

fn property_value(node: &GraphNode4D, property: &str) -> Value {
    match property {
        "id" => json!(node.id),
        "x" => json!(node.x),
        "y" => json!(node.y),
        "z" => json!(node.z),
        "begin_ts" => json!(node.begin_ts),
        "end_ts" => json!(node.end_ts),
        other => node.properties.get(other).cloned().unwrap_or(Value::Null),
    }
}

fn parse_context(query: &str) -> Result<TraversalContext4D, GeoCypherError> {
    let mut ctx = TraversalContext4D::default();
    let upper = query.to_ascii_uppercase();
    if !upper.contains(" WHERE ") {
        return Ok(ctx);
    }

    if let Some(time_call) = extract_call(query, "time_window")? {
        ctx.time_window = Some(parse_time_window(time_call)?);
    }
    if let Some(spatial_call) = extract_call(query, "spatial_sphere")? {
        ctx.spatial_region = Some(parse_spatial_sphere(spatial_call)?);
    }

    Ok(ctx)
}

fn parse_time_window(input: &str) -> Result<TemporalWindow, GeoCypherError> {
    let args = between(input, "(", ")")?;
    let parts = split_top_level(args, ',');
    if parts.len() != 2 {
        return Err(GeoCypherError::new("time_window expects start and end"));
    }
    Ok(TemporalWindow {
        start: parse_u64(parts[0])?,
        end: parse_u64(parts[1])?,
    })
}

fn parse_spatial_sphere(input: &str) -> Result<SpatialRegion, GeoCypherError> {
    let args = between(input, "(", ")")?;
    let parts = split_top_level(args, ',');
    if parts.len() != 2 {
        return Err(GeoCypherError::new(
            "spatial_sphere expects [x,y,z] and radius",
        ));
    }

    let coords = parse_vec3(parts[0])?;
    let radius = parts[1]
        .trim()
        .parse::<f32>()
        .map_err(|_| GeoCypherError::new("invalid spatial_sphere radius"))?;

    Ok(SpatialRegion::Sphere {
        center: coords,
        radius,
    })
}

fn parse_vec3(input: &str) -> Result<Vec3, GeoCypherError> {
    let trimmed = input.trim();
    let inner = trimmed
        .strip_prefix('[')
        .and_then(|value| value.strip_suffix(']'))
        .ok_or_else(|| GeoCypherError::new("expected [x,y,z] vector"))?;
    let parts: Vec<_> = inner.split(',').map(str::trim).collect();
    if parts.len() != 3 {
        return Err(GeoCypherError::new("expected three vector coordinates"));
    }

    Ok(Vec3::new(
        parse_f32(parts[0])?,
        parse_f32(parts[1])?,
        parse_f32(parts[2])?,
    ))
}

fn parse_f32(input: &str) -> Result<f32, GeoCypherError> {
    input
        .trim()
        .parse::<f32>()
        .map_err(|_| GeoCypherError::new("invalid float"))
}

fn parse_u64(input: &str) -> Result<u64, GeoCypherError> {
    input
        .trim()
        .parse::<u64>()
        .map_err(|_| GeoCypherError::new("invalid integer"))
}

fn parse_named_u64(input: &str, name: &str) -> Result<u64, GeoCypherError> {
    let (key, value) = input
        .split_once(':')
        .ok_or_else(|| GeoCypherError::new("expected named argument"))?;
    if !key.trim().eq_ignore_ascii_case(name) {
        return Err(GeoCypherError::new(format!("expected {name} argument")));
    }
    parse_u64(value)
}

fn normalize_query(query: &str) -> String {
    query.split_whitespace().collect::<Vec<_>>().join(" ")
}

fn between<'a>(input: &'a str, start: &str, end: &str) -> Result<&'a str, GeoCypherError> {
    let start_idx = input
        .find(start)
        .ok_or_else(|| GeoCypherError::new("missing opening delimiter"))?
        + start.len();
    let end_idx = input[start_idx..]
        .rfind(end)
        .ok_or_else(|| GeoCypherError::new("missing closing delimiter"))?
        + start_idx;
    Ok(&input[start_idx..end_idx])
}

fn extract_call<'a>(query: &'a str, name: &str) -> Result<Option<&'a str>, GeoCypherError> {
    let Some(start_idx) = query.to_ascii_lowercase().find(name) else {
        return Ok(None);
    };
    let tail = &query[start_idx..];
    let open = tail
        .find('(')
        .ok_or_else(|| GeoCypherError::new("missing function opening paren"))?;
    let mut depth = 0usize;
    for (idx, ch) in tail[open..].char_indices() {
        match ch {
            '(' => depth += 1,
            ')' => {
                depth -= 1;
                if depth == 0 {
                    return Ok(Some(&tail[..open + idx + 1]));
                }
            }
            _ => {}
        }
    }
    Err(GeoCypherError::new("unterminated function call"))
}

fn split_top_level(input: &str, delimiter: char) -> Vec<&str> {
    let mut parts = Vec::new();
    let mut start = 0usize;
    let mut paren_depth = 0usize;
    let mut bracket_depth = 0usize;

    for (idx, ch) in input.char_indices() {
        match ch {
            '(' => paren_depth += 1,
            ')' => paren_depth = paren_depth.saturating_sub(1),
            '[' => bracket_depth += 1,
            ']' => bracket_depth = bracket_depth.saturating_sub(1),
            value if value == delimiter && paren_depth == 0 && bracket_depth == 0 => {
                parts.push(input[start..idx].trim());
                start = idx + ch.len_utf8();
            }
            _ => {}
        }
    }
    parts.push(input[start..].trim());
    parts
}

fn node_is_valid(node: &GraphNode4D, ctx: &TraversalContext4D) -> bool {
    if let Some(window) = ctx.time_window {
        if !window.overlaps(node.begin_ts, node.end_ts) {
            return false;
        }
    }
    if let Some(region) = ctx.spatial_region {
        if !region.contains(node.position()) {
            return false;
        }
    }
    true
}

fn edge_is_valid(edge: TemporalEdge, ctx: &TraversalContext4D) -> bool {
    ctx.time_window
        .map(|window| window.overlaps(edge.begin_ts, edge.end_ts))
        .unwrap_or(true)
}
