use std::cmp::Ordering;
use std::time::Instant;

use halley_core::field::{NodeId, NodeKind as FieldNodeKind, NodeState as FieldNodeState};
use halley_ipc::{
    IpcError, NodeInfo, NodeKind, NodeListResponse, NodeMoveDirection, NodeOutputGroup,
    NodeProtocolFamily, NodeRelationInfo, NodeRequest, NodeRole, NodeSelector, NodeState, Response,
};
use smithay::desktop::PopupManager;
use smithay::reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::{XdgPopupSurfaceData, XdgToplevelSurfaceData};

use crate::compositor::actions::window::promote_node_level;
use crate::compositor::root::Halley;
use crate::compositor::surface::{current_surface_size_for_node, request_close_node_toplevel};

use super::{focus_output_if_needed, sorted_outputs, validate_output};

pub(super) fn handle_node_request(st: &mut Halley, request: NodeRequest) -> Response {
    match request {
        NodeRequest::List { output } => match list_nodes(st, output.as_deref()) {
            Ok(outputs) => Response::NodeList(outputs),
            Err(err) => Response::Error(err),
        },
        NodeRequest::Info { selector, output } => {
            match resolve_node_selector(st, selector.as_ref(), output.as_deref()) {
                Ok(id) => Response::NodeInfo(node_info(st, id)),
                Err(err) => Response::Error(err),
            }
        }
        NodeRequest::Focus { selector, output } => {
            match resolve_node_selector(st, selector.as_ref(), output.as_deref()) {
                Ok(id) => match focus_node(st, id, Instant::now()) {
                    Ok(()) => Response::Ok,
                    Err(err) => Response::Error(err),
                },
                Err(err) => Response::Error(err),
            }
        }
        NodeRequest::Move {
            direction,
            selector,
            output,
        } => match resolve_node_selector(st, selector.as_ref(), output.as_deref()) {
            Ok(id) => match move_node_direction(st, id, direction) {
                Ok(()) => Response::Ok,
                Err(err) => Response::Error(err),
            },
            Err(err) => Response::Error(err),
        },
        NodeRequest::Close { selector, output } => {
            match resolve_node_selector(st, selector.as_ref(), output.as_deref()) {
                Ok(id) => {
                    if request_close_node_toplevel(st, id) {
                        Response::Ok
                    } else {
                        Response::Error(IpcError::NotFound(format!(
                            "node {} does not have a closeable toplevel surface",
                            id.as_u64()
                        )))
                    }
                }
                Err(err) => Response::Error(err),
            }
        }
    }
}

fn list_nodes(st: &Halley, output: Option<&str>) -> Result<NodeListResponse, IpcError> {
    let outputs: Vec<String> = match output {
        Some(name) => vec![validate_output(st, name)?.to_string()],
        None => sorted_outputs(st),
    };
    let groups = outputs
        .into_iter()
        .map(|output| NodeOutputGroup {
            nodes: surface_nodes_on_output(st, output.as_str())
                .into_iter()
                .map(|id| node_info(st, id))
                .collect(),
            output,
        })
        .collect();
    Ok(NodeListResponse { outputs: groups })
}

pub(super) fn focus_node(st: &mut Halley, id: NodeId, now: Instant) -> Result<(), IpcError> {
    if !node_is_queryable_surface(st, id) {
        return Err(IpcError::NotFound(format!(
            "node {} is not available",
            id.as_u64()
        )));
    }
    let node = st
        .model
        .field
        .node(id)
        .cloned()
        .ok_or_else(|| IpcError::NotFound(format!("node {} is not available", id.as_u64())))?;
    let output = st
        .model
        .monitor_state
        .node_monitor
        .get(&id)
        .cloned()
        .ok_or_else(|| IpcError::NotFound(format!("node {} has no output", id.as_u64())))?;
    focus_output_if_needed(st, output.as_str(), now);
    match node.state {
        FieldNodeState::Active | FieldNodeState::Drifting => {
            st.set_interaction_focus(Some(id), 30_000, now);
            let _ = st.animate_viewport_center_to(node.pos, now);
            Ok(())
        }
        FieldNodeState::Node => {
            if promote_node_level(st, id, now) {
                Ok(())
            } else {
                Err(IpcError::Internal(format!(
                    "failed to focus node {}",
                    id.as_u64()
                )))
            }
        }
        FieldNodeState::Core => Err(IpcError::Unsupported(format!(
            "node {} is a core node and cannot be focused via node commands",
            id.as_u64()
        ))),
    }
}

fn move_node_direction(
    st: &mut Halley,
    id: NodeId,
    direction: NodeMoveDirection,
) -> Result<(), IpcError> {
    let node = st
        .model
        .field
        .node(id)
        .cloned()
        .ok_or_else(|| IpcError::NotFound(format!("node {} not found", id.as_u64())))?;
    let step = 80.0;
    let (dx, dy) = match direction {
        NodeMoveDirection::Left => (-step, 0.0),
        NodeMoveDirection::Right => (step, 0.0),
        NodeMoveDirection::Up => (0.0, step),
        NodeMoveDirection::Down => (0.0, -step),
    };
    let to = halley_core::field::Vec2 {
        x: node.pos.x + dx,
        y: node.pos.y + dy,
    };
    let _ = st.model.field.set_pinned(id, false);
    crate::compositor::carry::system::begin_carry_state_tracking(st, id);
    let moved = st.carry_surface_non_overlap(id, to, false);
    if moved {
        crate::compositor::carry::system::update_carry_state_preview(st, id, Instant::now());
        st.set_interaction_focus(Some(id), 30_000, Instant::now());
    }
    crate::compositor::carry::system::end_carry_state_tracking(st, id);
    if moved {
        Ok(())
    } else {
        Err(IpcError::Internal(format!(
            "failed to move node {}",
            id.as_u64()
        )))
    }
}

pub(super) fn resolve_node_selector(
    st: &Halley,
    selector: Option<&NodeSelector>,
    output: Option<&str>,
) -> Result<NodeId, IpcError> {
    let requested_output = output.map(|name| validate_output(st, name)).transpose()?;
    match selector {
        None => resolve_default_node(st, requested_output.as_deref()),
        Some(NodeSelector::Focused) => resolve_focused_node(st, requested_output.as_deref()),
        Some(NodeSelector::Latest) => resolve_latest_node(st, requested_output.as_deref()),
        Some(NodeSelector::Id(id)) => resolve_node_match(
            st,
            requested_output.as_deref(),
            &format!("id:{id}"),
            |node_id| node_id.as_u64() == *id,
        ),
        Some(NodeSelector::Title(text)) => resolve_node_match(
            st,
            requested_output.as_deref(),
            &format!("title:{text}"),
            |node_id| {
                st.model
                    .field
                    .node(node_id)
                    .is_some_and(|node| contains_case_insensitive(node.label.as_str(), text))
            },
        ),
        Some(NodeSelector::App(text)) => resolve_node_match(
            st,
            requested_output.as_deref(),
            &format!("app:{text}"),
            |node_id| {
                st.model
                    .node_app_ids
                    .get(&node_id)
                    .is_some_and(|app_id| contains_case_insensitive(app_id.as_str(), text))
            },
        ),
    }
}

fn resolve_default_node(st: &Halley, output: Option<&str>) -> Result<NodeId, IpcError> {
    let output = output.unwrap_or_else(|| st.focused_monitor());
    resolve_focused_node(st, Some(output))
        .or_else(|_| {
            st.last_focused_surface_node_for_monitor(output)
                .filter(|&id| node_is_queryable_surface(st, id))
                .ok_or_else(|| {
                    IpcError::NotFound(format!("no last-focused node on output {output}"))
                })
        })
        .or_else(|_| resolve_latest_node(st, Some(output)))
}

fn resolve_focused_node(st: &Halley, output: Option<&str>) -> Result<NodeId, IpcError> {
    let output = output.unwrap_or_else(|| st.focused_monitor());
    st.model
        .focus_state
        .primary_interaction_focus
        .filter(|&id| node_matches_output(st, id, output) && node_is_queryable_surface(st, id))
        .ok_or_else(|| IpcError::NotFound(format!("no focused node on output {output}")))
}

fn resolve_latest_node(st: &Halley, output: Option<&str>) -> Result<NodeId, IpcError> {
    let output = output.unwrap_or_else(|| st.focused_monitor());
    surface_nodes_on_output(st, output)
        .into_iter()
        .max_by_key(|id| id.as_u64())
        .ok_or_else(|| IpcError::NotFound(format!("no nodes on output {output}")))
}

fn resolve_node_match<F>(
    st: &Halley,
    output: Option<&str>,
    label: &str,
    predicate: F,
) -> Result<NodeId, IpcError>
where
    F: Fn(NodeId) -> bool,
{
    let candidates: Vec<NodeId> = surface_nodes(st, output)
        .into_iter()
        .filter(|&id| predicate(id))
        .collect();
    match candidates.as_slice() {
        [id] => Ok(*id),
        [] => Err(IpcError::NotFound(format!(
            "no node matched selector {label}"
        ))),
        many => Err(IpcError::Ambiguous(format!(
            "selector {label} matched multiple nodes: {}",
            many.iter()
                .map(|id| format!(
                    "{} ({})",
                    id.as_u64(),
                    st.model
                        .field
                        .node(*id)
                        .map(|n| n.label.as_str())
                        .unwrap_or("unknown")
                ))
                .collect::<Vec<_>>()
                .join(", ")
        ))),
    }
}

pub(super) fn node_info(st: &Halley, id: NodeId) -> NodeInfo {
    let node = st
        .model
        .field
        .node(id)
        .expect("node info requires live node");
    let size = current_surface_size_for_node(st, id).unwrap_or(node.intrinsic_size);
    let output = st.model.monitor_state.node_monitor.get(&id).cloned();
    let metadata = node_surface_metadata(st, id);
    let latest = output.as_deref().and_then(|output| {
        surface_nodes_on_output(st, output)
            .into_iter()
            .max_by_key(|candidate| candidate.as_u64())
    }) == Some(id);
    NodeInfo {
        id: id.as_u64(),
        title: node.label.clone(),
        app_id: st.model.node_app_ids.get(&id).cloned(),
        output,
        kind: match node.kind {
            FieldNodeKind::Surface => NodeKind::Surface,
            FieldNodeKind::Core => NodeKind::Core,
        },
        state: match node.state {
            FieldNodeState::Active => NodeState::Active,
            FieldNodeState::Drifting => NodeState::Drifting,
            FieldNodeState::Node => NodeState::Node,
            FieldNodeState::Core => NodeState::Core,
        },
        visible: st.model.field.is_visible(id),
        focused: st.model.focus_state.primary_interaction_focus == Some(id),
        latest,
        role: metadata.role,
        protocol_family: metadata.protocol_family,
        modal: metadata.modal,
        parent: metadata.parent,
        transient_for: metadata.transient_for,
        child_popup_count: metadata.child_popup_count,
        pos_x: node.pos.x,
        pos_y: node.pos.y,
        width: size.x,
        height: size.y,
    }
}

#[derive(Debug, Clone)]
struct NodeSurfaceMetadata {
    role: NodeRole,
    protocol_family: NodeProtocolFamily,
    modal: bool,
    parent: Option<NodeRelationInfo>,
    transient_for: Option<NodeRelationInfo>,
    child_popup_count: usize,
}

fn node_surface_metadata(st: &Halley, id: NodeId) -> NodeSurfaceMetadata {
    let Some(surface) = node_root_surface(st, id) else {
        return NodeSurfaceMetadata {
            role: NodeRole::Unknown,
            protocol_family: NodeProtocolFamily::Unknown,
            modal: false,
            parent: None,
            transient_for: None,
            child_popup_count: 0,
        };
    };

    let child_popup_count = PopupManager::popups_for_surface(&surface).count();
    let xdg_toplevel = with_states(&surface, |states| {
        states.data_map.get::<XdgToplevelSurfaceData>().map(|data| {
            let guard = data.lock().expect("xdg toplevel data");
            (guard.parent.clone(), guard.modal)
        })
    });
    if let Some((parent_surface, modal)) = xdg_toplevel {
        let relation = parent_surface
            .as_ref()
            .map(|surface| relation_for_surface(st, surface));
        let role = if modal || relation.is_some() {
            NodeRole::Dialog
        } else {
            NodeRole::NormalToplevel
        };
        return NodeSurfaceMetadata {
            role,
            protocol_family: NodeProtocolFamily::XdgToplevel,
            modal,
            parent: relation.clone(),
            transient_for: relation,
            child_popup_count,
        };
    }

    let xdg_popup = with_states(&surface, |states| {
        states
            .data_map
            .get::<XdgPopupSurfaceData>()
            .map(|data| data.lock().expect("xdg popup data").parent.clone())
    });
    if let Some(parent_surface) = xdg_popup {
        let relation = parent_surface
            .as_ref()
            .map(|surface| relation_for_surface(st, surface));
        return NodeSurfaceMetadata {
            role: NodeRole::Popup,
            protocol_family: NodeProtocolFamily::XdgPopup,
            modal: false,
            parent: relation.clone(),
            transient_for: relation,
            child_popup_count,
        };
    }

    NodeSurfaceMetadata {
        role: NodeRole::Unknown,
        protocol_family: NodeProtocolFamily::Unknown,
        modal: false,
        parent: None,
        transient_for: None,
        child_popup_count,
    }
}

fn node_root_surface(st: &Halley, id: NodeId) -> Option<WlSurface> {
    st.platform
        .xdg_shell_state
        .toplevel_surfaces()
        .iter()
        .find_map(|surface| {
            let surface_id = surface.wl_surface().id();
            (st.model.surface_to_node.get(&surface_id).copied() == Some(id))
                .then(|| surface.wl_surface().clone())
        })
        .or_else(|| {
            st.platform
                .xdg_shell_state
                .popup_surfaces()
                .iter()
                .find_map(|surface| {
                    let surface_id = surface.wl_surface().id();
                    (st.model.surface_to_node.get(&surface_id).copied() == Some(id))
                        .then(|| surface.wl_surface().clone())
                })
        })
}

fn relation_for_surface(st: &Halley, surface: &WlSurface) -> NodeRelationInfo {
    NodeRelationInfo {
        node_id: st
            .model
            .surface_to_node
            .get(&surface.id())
            .map(|id| id.as_u64()),
    }
}

pub(super) fn node_is_queryable_surface(st: &Halley, id: NodeId) -> bool {
    st.model
        .field
        .node(id)
        .is_some_and(|node| st.model.field.is_visible(id) && node.kind == FieldNodeKind::Surface)
}

pub(super) fn node_matches_output(st: &Halley, id: NodeId, output: &str) -> bool {
    st.model
        .monitor_state
        .node_monitor
        .get(&id)
        .map(|name| name.as_str())
        == Some(output)
}

fn surface_nodes(st: &Halley, output: Option<&str>) -> Vec<NodeId> {
    let mut nodes: Vec<NodeId> = st
        .model
        .field
        .node_ids_all()
        .into_iter()
        .filter_map(|id| {
            let node = st.model.field.node(id)?;
            (node.kind == FieldNodeKind::Surface
                && st.model.field.is_visible(id)
                && output
                    .map(|output| node_matches_output(st, id, output))
                    .unwrap_or(true))
            .then_some(id)
        })
        .collect();
    nodes.sort_by(|a, b| {
        let output_cmp = match (
            st.model.monitor_state.node_monitor.get(a),
            st.model.monitor_state.node_monitor.get(b),
        ) {
            (Some(a_output), Some(b_output)) => a_output.cmp(b_output),
            (Some(_), None) => Ordering::Less,
            (None, Some(_)) => Ordering::Greater,
            (None, None) => Ordering::Equal,
        };
        output_cmp.then(a.as_u64().cmp(&b.as_u64()))
    });
    nodes
}

fn surface_nodes_on_output(st: &Halley, output: &str) -> Vec<NodeId> {
    surface_nodes(st, Some(output))
}

fn contains_case_insensitive(haystack: &str, needle: &str) -> bool {
    haystack
        .to_ascii_lowercase()
        .contains(needle.to_ascii_lowercase().as_str())
}
