use std::collections::HashMap;

use ratatui::layout::{Margin, Rect};

use crate::input::{ActiveTab, HoverTarget, UiState};
use crate::layout::form as form_layout;
use crate::layout::form::FormFieldLayout;
use crate::pipeline::{FieldInstanceId, FieldSemantics, ValidationState};
use crate::query::form::OrderedArg;
use crate::spec::CommandPath;

pub(crate) const MAX_DROPDOWN_ROWS: u16 = 6;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct DropdownGeometry {
    pub(crate) rect: Rect,
    pub(crate) visible_rows: usize,
}

#[derive(Debug, Default, Clone)]
pub struct FrameLayout {
    pub sidebar: Option<Rect>,
    pub sidebar_list: Option<Rect>,
    pub form: Option<Rect>,
    pub search: Option<Rect>,
    pub preview: Option<Rect>,
    pub footer: Option<Rect>,
    pub dropdown: Option<Rect>,
    pub sidebar_items: Vec<SidebarItemLayout>,
    pub form_fields: Vec<FormFieldLayout>,
    pub form_inputs: HashMap<String, Rect>,
    pub form_view: Option<Rect>,
    pub form_tabs: Vec<TabButtonLayout>,
    pub footer_buttons: Vec<FooterButtonLayout>,
    pub invalid_field_ids: Vec<String>,
}

#[derive(Debug, Default, Clone)]
pub struct FrameSnapshot {
    pub layout: FrameLayout,
    pub form_scroll_max: u16,
    pub help_scroll_max: u16,
}

impl FrameSnapshot {
    pub fn first_invalid_field_id(&self) -> Option<&str> {
        self.layout.invalid_field_ids.first().map(String::as_str)
    }

    pub fn form_scroll(&self, requested_scroll: u16) -> u16 {
        requested_scroll.min(self.form_scroll_max)
    }

    pub fn help_scroll(&self, requested_scroll: u16) -> u16 {
        requested_scroll.min(self.help_scroll_max)
    }

    pub fn footer_target_at(&self, x: u16, y: u16) -> Option<HoverTarget> {
        self.layout
            .footer_buttons
            .iter()
            .find(|button| contains(button.rect, x, y))
            .map(|button| button.target)
    }

    pub fn tab_at(&self, x: u16, y: u16) -> Option<ActiveTab> {
        self.layout
            .form_tabs
            .iter()
            .find(|tab| contains(tab.rect, x, y))
            .map(|tab| tab.tab)
    }

    pub fn sidebar_item_at(&self, x: u16, y: u16) -> Option<&SidebarItemLayout> {
        self.layout
            .sidebar_items
            .iter()
            .find(|item| contains(item.row, x, y))
    }

    pub fn form_input_rect(&self, arg_id: &str) -> Option<Rect> {
        self.layout.form_inputs.get(arg_id).copied()
    }

    pub fn form_field_layout(&self, arg_id: &str) -> Option<&FormFieldLayout> {
        self.layout
            .form_fields
            .iter()
            .find(|field| field.arg_id == arg_id)
    }

    pub fn form_field_at(&self, x: u16, y: u16) -> Option<FormFieldHitLayout> {
        self.layout.form_fields.iter().find_map(|field| {
            let in_input = contains(field.input, x, y);
            let in_label = field.label.is_some_and(|label| contains(label, x, y));
            let in_description = field
                .description
                .is_some_and(|description| contains(description, x, y));

            (in_input || in_label || in_description).then(|| FormFieldHitLayout {
                arg_id: field.arg_id.clone(),
                in_input,
                in_label,
                in_description,
            })
        })
    }

    pub fn form_view_rect(&self) -> Option<Rect> {
        self.layout.form_view
    }

    pub fn dropdown_contains(&self, x: u16, y: u16) -> bool {
        self.layout
            .dropdown
            .is_some_and(|area| contains(area, x, y))
    }

    pub fn preview_contains(&self, x: u16, y: u16) -> bool {
        self.layout.preview.is_some_and(|area| contains(area, x, y))
    }

    pub fn search_contains(&self, x: u16, y: u16) -> bool {
        self.layout.search.is_some_and(|area| contains(area, x, y))
    }

    pub fn sidebar_contains(&self, x: u16, y: u16) -> bool {
        self.layout.sidebar.is_some_and(|area| contains(area, x, y))
    }

    pub fn sidebar_visible_rows(&self) -> Option<usize> {
        self.layout
            .sidebar_list
            .map(|area| usize::from(area.height))
    }

    pub fn form_contains(&self, x: u16, y: u16) -> bool {
        self.layout.form.is_some_and(|area| contains(area, x, y))
    }

    pub fn form_content_y(&self, row: u16, scroll: u16) -> Option<u16> {
        let form_view = self.layout.form_view?;
        if row < form_view.y || row >= form_view.y + form_view.height {
            return None;
        }
        Some(row.saturating_sub(form_view.y).saturating_add(scroll))
    }

    pub fn input_position_from_point(
        &self,
        arg_id: &str,
        x: u16,
        y: u16,
        clamp: bool,
    ) -> Option<(u16, u16)> {
        let input_rect = self.form_input_rect(arg_id)?;
        let inner_x = input_rect.x.saturating_add(1);
        let inner_y = input_rect.y.saturating_add(1);
        let inner_w = input_rect.width.saturating_sub(2);
        let inner_h = input_rect.height.saturating_sub(2);
        if inner_w == 0 || inner_h == 0 {
            return None;
        }
        if !clamp
            && (x < inner_x || y < inner_y || x >= inner_x + inner_w || y >= inner_y + inner_h)
        {
            return None;
        }
        let x = if clamp {
            x.clamp(inner_x, inner_x + inner_w - 1)
        } else {
            x
        };
        let y = if clamp {
            y.clamp(inner_y, inner_y + inner_h - 1)
        } else {
            y
        };
        Some((
            y.saturating_sub(inner_y).min(inner_h.saturating_sub(1)),
            x.saturating_sub(inner_x).min(inner_w.saturating_sub(1)),
        ))
    }

    pub fn dropdown_choice_index(&self, row: u16, scroll: usize) -> Option<usize> {
        let area = self.layout.dropdown?;
        if row <= area.y || row >= area.y + area.height - 1 {
            return None;
        }
        Some(usize::from(row.saturating_sub(area.y + 1)) + scroll)
    }

    pub fn dropdown_visible_rows(&self) -> Option<usize> {
        self.layout
            .dropdown
            .map(|dropdown| usize::from(dropdown.height.saturating_sub(2)))
    }

    pub fn dropdown_geometry_for_input(
        &self,
        arg_id: &str,
        total_options: usize,
    ) -> Option<DropdownGeometry> {
        self.layout
            .form_view
            .zip(self.form_input_rect(arg_id))
            .and_then(|(form_view, input_rect)| {
                dropdown_geometry(form_view, input_rect, total_options)
            })
    }
}

pub(crate) fn dropdown_geometry(
    form_view: Rect,
    input_rect: Rect,
    total_options: usize,
) -> Option<DropdownGeometry> {
    if total_options == 0 || form_view.height == 0 || form_view.width < 3 || input_rect.width < 3 {
        return None;
    }

    let desired_rows = total_options.min(MAX_DROPDOWN_ROWS as usize);
    let available_below = form_view
        .y
        .saturating_add(form_view.height)
        .saturating_sub(input_rect.y.saturating_add(input_rect.height));
    let available_above = input_rect.y.saturating_sub(form_view.y);

    let rows_below = available_below.saturating_sub(2) as usize;
    let rows_above = available_above.saturating_sub(2) as usize;

    let place_below = if rows_below >= desired_rows || rows_above == 0 {
        rows_below > 0
    } else if rows_above >= desired_rows || rows_below == 0 {
        false
    } else {
        rows_below >= rows_above
    };

    let visible_rows = if place_below { rows_below } else { rows_above }.min(desired_rows);
    if visible_rows == 0 {
        return None;
    }

    let popup_height = u16::try_from(visible_rows).unwrap_or(MAX_DROPDOWN_ROWS) + 2;
    let y = if place_below {
        input_rect.y.saturating_add(input_rect.height)
    } else {
        input_rect.y.saturating_sub(popup_height)
    };

    let width = input_rect.width.min(form_view.width);
    let max_x = form_view
        .x
        .saturating_add(form_view.width.saturating_sub(width));
    let x = input_rect.x.clamp(form_view.x, max_x);

    Some(DropdownGeometry {
        rect: Rect::new(x, y, width, popup_height),
        visible_rows,
    })
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn populate_form_layout(
    ui: &UiState,
    area: Rect,
    active_args: &[OrderedArg<'_>],
    help: &str,
    validation: &ValidationState,
    field_semantics: &std::collections::BTreeMap<FieldInstanceId, FieldSemantics>,
    input_height_overrides: &std::collections::HashMap<String, u16>,
    label_height_overrides: &std::collections::HashMap<String, u16>,
    frame_snapshot: &mut FrameSnapshot,
) {
    let content_area = area;
    let content_height = form_layout::measure_fields_height_with_layout_overrides_and_semantics(
        active_args,
        &validation.field_errors,
        input_height_overrides,
        label_height_overrides,
        field_semantics,
    );
    let help_height = form_layout::measure_help_height(help);
    let viewport_height = content_area.height;
    let help_viewport_height = help_overlay_content_rect(content_area).height;
    frame_snapshot.form_scroll_max = content_height.saturating_sub(viewport_height);
    frame_snapshot.help_scroll_max = help_height.saturating_sub(help_viewport_height);
    let form_scroll = ui.form_scroll(frame_snapshot);
    let frame_layout = &mut frame_snapshot.layout;
    frame_layout.form = Some(area);
    frame_layout.dropdown = None;
    frame_layout.form_fields.clear();
    frame_layout.form_inputs.clear();
    frame_layout.form_tabs.clear();
    frame_layout.invalid_field_ids.clear();
    frame_layout.form_view = Some(content_area);

    let form_fields = form_layout::project_visible_form_fields(&form_layout::FormProjectionInput {
        area,
        content_area,
        form_scroll,
        active_args,
        field_errors: &validation.field_errors,
        input_height_overrides,
        label_height_overrides,
        field_semantics,
    });
    for field in form_fields {
        frame_layout
            .form_inputs
            .insert(field.arg_id.clone(), field.input);
        if validation.field_errors.contains_key(&field.arg_id) {
            frame_layout.invalid_field_ids.push(field.arg_id.clone());
        }

        if ui.dropdown_open.as_deref() == Some(&field.arg_id) {
            let choice_count = active_args
                .iter()
                .find(|item| item.arg.id == field.arg_id)
                .map_or(0, |item| {
                    let required = field_semantics
                        .get(&FieldInstanceId::from_arg(item.arg))
                        .map_or(item.arg.required, |semantics| semantics.required_badge);
                    let clear_offset =
                        usize::from(crate::query::form::single_choice_has_clear_row(
                            item.arg,
                            item.widget,
                            required,
                        ));
                    item.arg.choices.len().saturating_add(clear_offset)
                });
            frame_layout.dropdown = frame_layout
                .form_view
                .and_then(|form_view| dropdown_geometry(form_view, field.input, choice_count))
                .map(|geometry| geometry.rect);
        }

        frame_layout.form_fields.push(field);
    }
}

pub(crate) fn help_overlay_popup_rect(area: Rect) -> Rect {
    let horizontal = u16::from(area.width > 24);
    let vertical = u16::from(area.height > 8);
    area.inner(Margin {
        horizontal,
        vertical,
    })
}

pub(crate) fn help_overlay_content_rect(area: Rect) -> Rect {
    let popup = help_overlay_popup_rect(area);
    let horizontal = u16::from(popup.width > 4);
    let vertical = u16::from(popup.height > 4);
    popup.inner(Margin {
        horizontal,
        vertical,
    })
}

fn contains(area: Rect, x: u16, y: u16) -> bool {
    x >= area.x && x < area.x + area.width && y >= area.y && y < area.y + area.height
}

#[cfg(test)]
mod tests {
    use ratatui::layout::Rect;

    use super::{
        FooterButtonLayout, FrameSnapshot, SidebarItemLayout, TabButtonLayout, dropdown_geometry,
    };
    use crate::input::{ActiveTab, HoverTarget};

    #[test]
    fn query_helpers_hit_expected_targets() {
        let mut snapshot = FrameSnapshot::default();
        snapshot.layout.footer_buttons = vec![FooterButtonLayout {
            target: HoverTarget::Run,
            rect: Rect::new(0, 10, 8, 1),
        }];
        snapshot.layout.form_tabs = vec![TabButtonLayout {
            tab: ActiveTab::Inputs,
            rect: Rect::new(0, 0, 8, 1),
        }];
        snapshot.layout.sidebar_items = vec![SidebarItemLayout {
            path: vec!["build".to_string()].into(),
            row: Rect::new(0, 2, 20, 1),
            has_children: true,
        }];

        assert_eq!(snapshot.footer_target_at(1, 10), Some(HoverTarget::Run));
        assert_eq!(snapshot.tab_at(1, 0), Some(ActiveTab::Inputs));
        assert_eq!(
            snapshot
                .sidebar_item_at(1, 2)
                .map(|item| item.path.as_slice()),
            Some(&["build".to_string()][..])
        );
    }

    #[test]
    fn input_and_dropdown_queries_respect_inner_geometry() {
        let mut snapshot = FrameSnapshot::default();
        snapshot
            .layout
            .form_inputs
            .insert("name".to_string(), Rect::new(10, 5, 12, 3));
        snapshot.layout.form_view = Some(Rect::new(0, 5, 40, 10));
        snapshot.layout.dropdown = Some(Rect::new(10, 8, 12, 5));

        assert_eq!(
            snapshot.input_position_from_point("name", 11, 6, false),
            Some((0, 0))
        );
        assert_eq!(snapshot.form_content_y(7, 3), Some(5));
        assert!(snapshot.dropdown_contains(11, 9));
        assert_eq!(snapshot.dropdown_choice_index(10, 2), Some(3));
        assert_eq!(snapshot.dropdown_visible_rows(), Some(3));
    }

    #[test]
    fn dropdown_geometry_matches_expected_popup_layout() {
        let form_view = Rect::new(10, 5, 60, 20);
        let input_rect = Rect::new(14, 8, 24, 3);
        let geometry = dropdown_geometry(form_view, input_rect, 4).expect("geometry");

        assert_eq!(geometry.rect.x, input_rect.x);
        assert_eq!(geometry.rect.width, input_rect.width);
        assert_eq!(geometry.rect.y, input_rect.y + input_rect.height);
        assert_eq!(geometry.visible_rows, 4);
    }

    #[test]
    #[allow(clippy::too_many_lines)]
    fn populate_form_layout_preserves_form_field_order_when_errors_are_present() {
        use crate::input::{ActiveTab, Focus, UiState};
        use crate::pipeline::ValidationState;
        use crate::query::form::visible_args;
        use crate::spec::{ArgKind, ArgSpec, CommandSpec, ValueCardinality};

        let command = CommandSpec {
            name: "tool".to_string(),
            version: None,
            about: None,
            help: String::new(),
            args: vec![
                ArgSpec {
                    id: "alpha".to_string(),
                    display_name: "--alpha".to_string(),
                    help: None,
                    required: true,
                    kind: ArgKind::Option,
                    default_values: Vec::new(),
                    choices: Vec::new(),
                    position: None,
                    value_cardinality: ValueCardinality::One,
                    value_hint: None,
                    ..ArgSpec::default()
                },
                ArgSpec {
                    id: "beta".to_string(),
                    display_name: "--beta".to_string(),
                    help: None,
                    required: true,
                    kind: ArgKind::Option,
                    default_values: Vec::new(),
                    choices: Vec::new(),
                    position: None,
                    value_cardinality: ValueCardinality::One,
                    value_hint: None,
                    ..ArgSpec::default()
                },
            ],
            subcommands: Vec::new(),
            ..CommandSpec::default()
        };
        let vm = crate::ui::screen::ScreenView {
            command: &command,
            root: &command,
            selected_path: crate::spec::CommandPath::default(),
            tree_rows: Vec::new(),
            sidebar_scroll: 0,
            active_args: visible_args(&command, ActiveTab::Inputs),
            authoritative_argv: Vec::new(),
            rendered_command: None,
            validation: ValidationState {
                is_valid: false,
                summary: Some("Missing required arguments: --alpha, --beta".to_string()),
                field_errors: std::collections::BTreeMap::from([
                    ("beta".to_string(), "Required argument".to_string()),
                    ("alpha".to_string(), "Required argument".to_string()),
                ]),
            },
            effective_values: std::collections::BTreeMap::new(),
            field_semantics: std::collections::BTreeMap::new(),
            inputs: None,
        };
        let mut snapshot = FrameSnapshot::default();
        let ui = UiState {
            focus: Focus::Sidebar,
            active_tab: ActiveTab::Inputs,
            last_non_help_tab: ActiveTab::Inputs,
            help_open: false,
            help_scroll: 0,
            selected_arg_index: 0,
            search_query: String::new(),
            editors: crate::editor_state::EditorState::default(),
            dropdown_open: None,
            dropdown_scroll: 0,
            dropdown_cursor: 0,
            sidebar_scroll: 0,
            form_scroll: 0,
            hover: None,
            hover_tab: None,
            mouse_select: None,
        };

        crate::frame_snapshot::populate_form_layout(
            &ui,
            Rect::new(0, 0, 40, 12),
            &vm.active_args,
            &vm.command.help,
            &vm.validation,
            &vm.field_semantics,
            &std::collections::HashMap::new(),
            &std::collections::HashMap::new(),
            &mut snapshot,
        );

        assert_eq!(
            snapshot
                .layout
                .form_fields
                .iter()
                .map(|field| field.arg_id.as_str())
                .collect::<Vec<_>>(),
            vec!["alpha", "beta"]
        );
        assert!(matches!(ui.focus, Focus::Sidebar));
    }

    #[test]
    fn inherited_section_descriptions_align_with_the_section_content() {
        use clap::{Arg, Command};

        use crate::input::{ActiveTab, Focus, UiState};
        use crate::pipeline::ValidationState;
        use crate::query::form::visible_args_for_path;
        use crate::spec::CommandPath;

        let root = crate::spec::CommandSpec::from_command(
            &Command::new("tool")
                .arg(Arg::new("config").long("config").help("Config file"))
                .subcommand(Command::new("clap-features").subcommand(Command::new("child"))),
        );
        let selected_path =
            CommandPath::from(vec!["clap-features".to_string(), "child".to_string()]);
        let active_args = visible_args_for_path(&root, &selected_path, ActiveTab::Inputs);
        let ui = UiState {
            focus: Focus::Sidebar,
            active_tab: ActiveTab::Inputs,
            last_non_help_tab: ActiveTab::Inputs,
            help_open: false,
            help_scroll: 0,
            selected_arg_index: 0,
            search_query: String::new(),
            editors: crate::editor_state::EditorState::default(),
            dropdown_open: None,
            dropdown_scroll: 0,
            dropdown_cursor: 0,
            sidebar_scroll: 0,
            form_scroll: 0,
            hover: None,
            hover_tab: None,
            mouse_select: None,
        };
        let mut snapshot = FrameSnapshot::default();

        crate::frame_snapshot::populate_form_layout(
            &ui,
            Rect::new(0, 0, 50, 12),
            &active_args,
            &root.help,
            &ValidationState::default(),
            &std::collections::BTreeMap::new(),
            &std::collections::HashMap::new(),
            &std::collections::HashMap::new(),
            &mut snapshot,
        );

        let inherited = snapshot
            .layout
            .form_fields
            .iter()
            .find(|field| field.arg_id == "config")
            .expect("inherited config field");

        assert_eq!(inherited.label.expect("label rect").x, 0);
        assert_eq!(
            inherited.description.expect("description rect").x,
            inherited.input.x
        );
    }
}

#[derive(Debug, Clone)]
pub struct FooterButtonLayout {
    pub target: HoverTarget,
    pub rect: Rect,
}

#[derive(Debug, Clone)]
pub struct SidebarItemLayout {
    pub path: CommandPath,
    pub row: Rect,
    pub has_children: bool,
}

#[derive(Debug, Clone)]
pub struct TabButtonLayout {
    pub tab: ActiveTab,
    pub rect: Rect,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormFieldHitLayout {
    pub arg_id: String,
    pub in_input: bool,
    pub in_label: bool,
    pub in_description: bool,
}
