use std::collections::HashMap;

use iced::widget::{Column, column, container, mouse_area, row, scrollable, space, text};
use iced::{
    Alignment, Background, Border, Color, Element, Length, Shadow, Subscription, Theme, Vector,
};
use muda::accelerator::{Accelerator, CMD_OR_CTRL, Code, Modifiers};
use muda::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem, Submenu};

use crate::app::Message;
use crate::components::{inputs, keyboard, popover};
use crate::state::tabs::TabType;
use bds_core::i18n::{UiLocale, translate};

const BLOG_SITE_ACTIONS: [MenuAction; 4] = [
    MenuAction::GenerateSitemap,
    MenuAction::ForceRenderSite,
    MenuAction::ValidateSite,
    MenuAction::UploadSite,
];

/// Every custom menu item that the application handles.
///
/// Edit commands are custom actions because Iced widgets are not native Cocoa
/// text responders; routing them through the app preserves native menu items
/// and shortcuts while operating on the focused Iced widget.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MenuAction {
    // File
    NewPost,
    ImportMedia,
    Save,
    OpenInBrowser,
    OpenDataFolder,
    ConnectServer,
    DisconnectServer,
    // Edit
    Undo,
    Redo,
    Cut,
    Copy,
    Paste,
    SelectAll,
    Find,
    Replace,
    EditPreferences,
    // View
    ViewPosts,
    ViewMedia,
    ToggleSidebar,
    TogglePanel,
    // Window
    PublishSelected,
    PreviewPost,
    EditMenu,
    RebuildDatabase,
    ReindexText,
    RebuildEmbeddingIndex,
    FindDuplicates,
    MetadataDiff,
    RegenerateCalendar,
    ValidateTranslations,
    FillMissingTranslations,
    GenerateSitemap,
    ForceRenderSite,
    ValidateSite,
    UploadSite,
    // Help
    About,
    OpenDocumentation,
    OpenApiDocumentation,
    OpenCliDocumentation,
    OpenMcpDocumentation,
    ViewOnGitHub,
    ReportIssue,
}

impl MenuAction {
    /// All variants in declaration order.
    pub const ALL: &'static [MenuAction] = &[
        MenuAction::NewPost,
        MenuAction::ImportMedia,
        MenuAction::Save,
        MenuAction::OpenInBrowser,
        MenuAction::OpenDataFolder,
        MenuAction::ConnectServer,
        MenuAction::DisconnectServer,
        MenuAction::Undo,
        MenuAction::Redo,
        MenuAction::Cut,
        MenuAction::Copy,
        MenuAction::Paste,
        MenuAction::SelectAll,
        MenuAction::Find,
        MenuAction::Replace,
        MenuAction::EditPreferences,
        MenuAction::ViewPosts,
        MenuAction::ViewMedia,
        MenuAction::ToggleSidebar,
        MenuAction::TogglePanel,
        MenuAction::PublishSelected,
        MenuAction::PreviewPost,
        MenuAction::EditMenu,
        MenuAction::RebuildDatabase,
        MenuAction::ReindexText,
        MenuAction::RebuildEmbeddingIndex,
        MenuAction::FindDuplicates,
        MenuAction::MetadataDiff,
        MenuAction::RegenerateCalendar,
        MenuAction::ValidateTranslations,
        MenuAction::FillMissingTranslations,
        MenuAction::GenerateSitemap,
        MenuAction::ForceRenderSite,
        MenuAction::ValidateSite,
        MenuAction::UploadSite,
        MenuAction::About,
        MenuAction::OpenDocumentation,
        MenuAction::OpenApiDocumentation,
        MenuAction::OpenCliDocumentation,
        MenuAction::OpenMcpDocumentation,
        MenuAction::ViewOnGitHub,
        MenuAction::ReportIssue,
    ];

    pub fn from_script_name(name: &str) -> Option<Self> {
        Some(match name {
            "new_post" => Self::NewPost,
            "import_media" => Self::ImportMedia,
            "save" => Self::Save,
            "open_in_browser" => Self::OpenInBrowser,
            "open_data_folder" => Self::OpenDataFolder,
            "connect_server" => Self::ConnectServer,
            "disconnect_server" => Self::DisconnectServer,
            "undo" => Self::Undo,
            "redo" => Self::Redo,
            "cut" => Self::Cut,
            "copy" => Self::Copy,
            "paste" => Self::Paste,
            "select_all" => Self::SelectAll,
            "find" => Self::Find,
            "replace" => Self::Replace,
            "edit_preferences" => Self::EditPreferences,
            "view_posts" => Self::ViewPosts,
            "view_media" => Self::ViewMedia,
            "toggle_sidebar" => Self::ToggleSidebar,
            "toggle_panel" => Self::TogglePanel,
            "publish_selected" => Self::PublishSelected,
            "preview_post" => Self::PreviewPost,
            "edit_menu" => Self::EditMenu,
            "rebuild_database" => Self::RebuildDatabase,
            "reindex_text" => Self::ReindexText,
            "rebuild_embedding_index" => Self::RebuildEmbeddingIndex,
            "find_duplicates" => Self::FindDuplicates,
            "metadata_diff" => Self::MetadataDiff,
            "regenerate_calendar" => Self::RegenerateCalendar,
            "validate_translations" => Self::ValidateTranslations,
            "fill_missing_translations" => Self::FillMissingTranslations,
            "generate_sitemap" => Self::GenerateSitemap,
            "force_render_site" => Self::ForceRenderSite,
            "validate_site" => Self::ValidateSite,
            "upload_site" => Self::UploadSite,
            "about" => Self::About,
            "open_documentation" => Self::OpenDocumentation,
            "open_api_documentation" => Self::OpenApiDocumentation,
            "view_on_github" => Self::ViewOnGitHub,
            "report_issue" => Self::ReportIssue,
            _ => return None,
        })
    }

    /// Return the i18n key for this action's menu label.
    pub fn i18n_key(self) -> &'static str {
        match self {
            Self::NewPost => "menu.item.newPost",
            Self::ImportMedia => "menu.item.importMedia",
            Self::Save => "menu.item.save",
            Self::OpenInBrowser => "menu.item.openInBrowser",
            Self::OpenDataFolder => "menu.item.openDataFolder",
            Self::ConnectServer => "menu.item.connectServer",
            Self::DisconnectServer => "menu.item.disconnectServer",
            Self::Undo => "menu.item.undo",
            Self::Redo => "menu.item.redo",
            Self::Cut => "menu.item.cut",
            Self::Copy => "menu.item.copy",
            Self::Paste => "menu.item.paste",
            Self::SelectAll => "menu.item.selectAll",
            Self::Find => "menu.item.find",
            Self::Replace => "menu.item.replace",
            Self::EditPreferences => "menu.item.editPreferences",
            Self::ViewPosts => "menu.item.viewPosts",
            Self::ViewMedia => "menu.item.viewMedia",
            Self::ToggleSidebar => "menu.item.toggleSidebar",
            Self::TogglePanel => "menu.item.togglePanel",
            Self::PublishSelected => "menu.item.publishSelected",
            Self::PreviewPost => "menu.item.previewPost",
            Self::EditMenu => "menu.item.editMenu",
            Self::RebuildDatabase => "menu.item.rebuildDatabase",
            Self::ReindexText => "menu.item.reindexText",
            Self::RebuildEmbeddingIndex => "menu.item.rebuildEmbeddingIndex",
            Self::FindDuplicates => "menu.item.findDuplicates",
            Self::MetadataDiff => "menu.item.metadataDiff",
            Self::RegenerateCalendar => "menu.item.regenerateCalendar",
            Self::ValidateTranslations => "menu.item.validateTranslations",
            Self::FillMissingTranslations => "menu.item.fillMissingTranslations",
            Self::GenerateSitemap => "menu.item.generateSitemap",
            Self::ForceRenderSite => "menu.item.forceRenderSite",
            Self::ValidateSite => "menu.item.validateSite",
            Self::UploadSite => "menu.item.uploadSite",
            Self::About => "menu.item.about",
            Self::OpenDocumentation => "menu.item.openDocumentation",
            Self::OpenApiDocumentation => "menu.item.openApiDocumentation",
            Self::OpenCliDocumentation => "menu.item.openCliDocumentation",
            Self::OpenMcpDocumentation => "menu.item.openMcpDocumentation",
            Self::ViewOnGitHub => "menu.item.viewOnGitHub",
            Self::ReportIssue => "menu.item.reportIssue",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowMenuGroup {
    File,
    Edit,
    View,
    Blog,
    Help,
}

impl WindowMenuGroup {
    const ALL: [Self; 5] = [Self::File, Self::Edit, Self::View, Self::Blog, Self::Help];

    fn i18n_key(self) -> &'static str {
        match self {
            Self::File => "menu.group.file",
            Self::Edit => "menu.group.edit",
            Self::View => "menu.group.view",
            Self::Blog => "menu.group.blog",
            Self::Help => "menu.group.help",
        }
    }

    fn mnemonic(self) -> char {
        match self {
            Self::File => 'f',
            Self::Edit => 'e',
            Self::View => 'v',
            Self::Blog => 'b',
            Self::Help => 'h',
        }
    }

    fn shifted(self, offset: isize) -> Self {
        let current = Self::ALL
            .iter()
            .position(|group| *group == self)
            .expect("window menu group is exhaustive") as isize;
        Self::ALL[(current + offset).rem_euclid(Self::ALL.len() as isize) as usize]
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WindowMenuEntry {
    Action(MenuAction),
    Separator,
}

#[derive(Debug)]
struct WindowMenuDefinition {
    group: WindowMenuGroup,
    entries: &'static [WindowMenuEntry],
}

use WindowMenuEntry::{Action, Separator};

const FILE_MENU: &[WindowMenuEntry] = &[
    Action(MenuAction::NewPost),
    Action(MenuAction::ImportMedia),
    Action(MenuAction::Save),
    Separator,
    Action(MenuAction::OpenInBrowser),
    Action(MenuAction::OpenDataFolder),
    Separator,
    Action(MenuAction::ConnectServer),
    Action(MenuAction::DisconnectServer),
];
const EDIT_MENU: &[WindowMenuEntry] = &[
    Action(MenuAction::Undo),
    Action(MenuAction::Redo),
    Separator,
    Action(MenuAction::Cut),
    Action(MenuAction::Copy),
    Action(MenuAction::Paste),
    Action(MenuAction::SelectAll),
    Separator,
    Action(MenuAction::Find),
    Action(MenuAction::Replace),
    Separator,
    Action(MenuAction::EditPreferences),
];
const VIEW_MENU: &[WindowMenuEntry] = &[
    Action(MenuAction::ViewPosts),
    Action(MenuAction::ViewMedia),
    Separator,
    Action(MenuAction::ToggleSidebar),
    Action(MenuAction::TogglePanel),
];
const BLOG_MENU: &[WindowMenuEntry] = &[
    Action(MenuAction::PublishSelected),
    Action(MenuAction::PreviewPost),
    Separator,
    Action(MenuAction::EditMenu),
    Separator,
    Action(MenuAction::RebuildDatabase),
    Action(MenuAction::ReindexText),
    Action(MenuAction::RebuildEmbeddingIndex),
    Action(MenuAction::FindDuplicates),
    Action(MenuAction::MetadataDiff),
    Separator,
    Action(MenuAction::RegenerateCalendar),
    Action(MenuAction::ValidateTranslations),
    Action(MenuAction::FillMissingTranslations),
    Separator,
    Action(MenuAction::GenerateSitemap),
    Action(MenuAction::ForceRenderSite),
    Action(MenuAction::ValidateSite),
    Action(MenuAction::UploadSite),
];
const HELP_MENU: &[WindowMenuEntry] = &[
    Action(MenuAction::About),
    Separator,
    Action(MenuAction::OpenDocumentation),
    Action(MenuAction::OpenApiDocumentation),
    Action(MenuAction::OpenCliDocumentation),
    Action(MenuAction::OpenMcpDocumentation),
    Action(MenuAction::ViewOnGitHub),
    Action(MenuAction::ReportIssue),
];

const WINDOW_MENU_GROUPS: &[WindowMenuDefinition] = &[
    WindowMenuDefinition {
        group: WindowMenuGroup::File,
        entries: FILE_MENU,
    },
    WindowMenuDefinition {
        group: WindowMenuGroup::Edit,
        entries: EDIT_MENU,
    },
    WindowMenuDefinition {
        group: WindowMenuGroup::View,
        entries: VIEW_MENU,
    },
    WindowMenuDefinition {
        group: WindowMenuGroup::Blog,
        entries: BLOG_MENU,
    },
    WindowMenuDefinition {
        group: WindowMenuGroup::Help,
        entries: HELP_MENU,
    },
];

fn window_menu(group: WindowMenuGroup) -> &'static WindowMenuDefinition {
    WINDOW_MENU_GROUPS
        .iter()
        .find(|definition| definition.group == group)
        .expect("window menu group is exhaustive")
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowMenuInput {
    AltChanged(bool),
    Mnemonic(char),
    Left,
    Right,
    Down,
    Up,
    Home,
    End,
    Activate,
    Escape,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowMenuEvent {
    Toggle(WindowMenuGroup),
    Hover(WindowMenuGroup),
    Dismiss,
    Input(WindowMenuInput),
    Action(MenuAction),
}

#[derive(Debug, Default)]
pub struct WindowMenuState {
    open_group: Option<WindowMenuGroup>,
    selected_index: Option<usize>,
    show_mnemonics: bool,
}

impl WindowMenuState {
    fn actions(&self) -> impl Iterator<Item = MenuAction> {
        self.open_group
            .into_iter()
            .flat_map(|group| window_menu(group).entries)
            .filter_map(|entry| match entry {
                Action(action) => Some(*action),
                Separator => None,
            })
    }

    fn selected_action(&self) -> Option<MenuAction> {
        self.selected_index
            .and_then(|index| self.actions().nth(index))
    }

    fn close(&mut self) {
        self.open_group = None;
        self.selected_index = None;
    }

    fn select_edge(&mut self, from_end: bool, enabled: impl Fn(MenuAction) -> bool) {
        let actions = self.actions().collect::<Vec<_>>();
        self.selected_index = if from_end {
            actions.iter().rposition(|action| enabled(*action))
        } else {
            actions.iter().position(|action| enabled(*action))
        };
    }

    fn advance(&mut self, offset: isize, enabled: impl Fn(MenuAction) -> bool) {
        let actions = self.actions().collect::<Vec<_>>();
        if actions.is_empty() {
            self.selected_index = None;
            return;
        }
        let start = self
            .selected_index
            .map_or(if offset > 0 { -1 } else { 0 }, |index| index as isize);
        self.selected_index = (1..=actions.len()).find_map(|step| {
            let index =
                (start + offset * step as isize).rem_euclid(actions.len() as isize) as usize;
            enabled(actions[index]).then_some(index)
        });
    }

    pub fn handle(
        &mut self,
        input: WindowMenuInput,
        enabled: impl Fn(MenuAction) -> bool + Copy,
    ) -> Option<MenuAction> {
        match input {
            WindowMenuInput::AltChanged(pressed) => self.show_mnemonics = pressed,
            WindowMenuInput::Mnemonic(key) => {
                if let Some(group) = WindowMenuGroup::ALL
                    .into_iter()
                    .find(|group| group.mnemonic() == key.to_ascii_lowercase())
                {
                    self.open_group = Some(group);
                    self.selected_index = None;
                }
            }
            WindowMenuInput::Left if self.open_group.is_some() => {
                self.open_group = self.open_group.map(|group| group.shifted(-1));
                self.selected_index = None;
            }
            WindowMenuInput::Right if self.open_group.is_some() => {
                self.open_group = self.open_group.map(|group| group.shifted(1));
                self.selected_index = None;
            }
            WindowMenuInput::Down if self.open_group.is_some() => self.advance(1, enabled),
            WindowMenuInput::Up if self.open_group.is_some() => self.advance(-1, enabled),
            WindowMenuInput::Home if self.open_group.is_some() => self.select_edge(false, enabled),
            WindowMenuInput::End if self.open_group.is_some() => self.select_edge(true, enabled),
            WindowMenuInput::Activate if self.open_group.is_some() => {
                let action = self.selected_action().filter(|action| enabled(*action));
                if action.is_some() {
                    self.close();
                }
                return action;
            }
            WindowMenuInput::Escape if self.open_group.is_some() => self.close(),
            _ => {}
        }
        None
    }

    pub fn handle_event(
        &mut self,
        event: WindowMenuEvent,
        enabled: impl Fn(MenuAction) -> bool + Copy,
    ) -> Option<MenuAction> {
        match event {
            WindowMenuEvent::Toggle(group) => {
                if self.open_group == Some(group) {
                    self.close();
                } else {
                    self.open_group = Some(group);
                    self.selected_index = None;
                }
                None
            }
            WindowMenuEvent::Hover(group) => {
                if self.open_group.is_some() && self.open_group != Some(group) {
                    self.open_group = Some(group);
                    self.selected_index = None;
                }
                None
            }
            WindowMenuEvent::Dismiss => {
                self.close();
                None
            }
            WindowMenuEvent::Input(input) => self.handle(input, enabled),
            WindowMenuEvent::Action(action) if enabled(action) => {
                self.close();
                Some(action)
            }
            WindowMenuEvent::Action(_) => None,
        }
    }
}

fn shortcut_hint(action: MenuAction) -> Option<&'static str> {
    Some(match action {
        MenuAction::NewPost => "Ctrl+N",
        MenuAction::ImportMedia => "Ctrl+I",
        MenuAction::Save => "Ctrl+S",
        MenuAction::Undo => "Ctrl+Z",
        MenuAction::Redo => "Ctrl+Shift+Z",
        MenuAction::Cut => "Ctrl+X",
        MenuAction::Copy => "Ctrl+C",
        MenuAction::Paste => "Ctrl+V",
        MenuAction::SelectAll => "Ctrl+A",
        MenuAction::Find => "Ctrl+F",
        MenuAction::Replace => "Ctrl+H",
        MenuAction::EditPreferences => "Ctrl+,",
        MenuAction::ViewPosts => "Ctrl+1",
        MenuAction::ViewMedia => "Ctrl+2",
        MenuAction::ToggleSidebar => "Ctrl+B",
        MenuAction::TogglePanel => "Ctrl+J",
        MenuAction::PublishSelected => "Ctrl+Shift+P",
        MenuAction::PreviewPost => "Ctrl+Shift+V",
        MenuAction::GenerateSitemap => "Ctrl+R",
        MenuAction::ForceRenderSite => "Ctrl+Shift+R",
        MenuAction::ValidateSite => "Ctrl+Shift+L",
        MenuAction::UploadSite => "Ctrl+Shift+U",
        _ => return None,
    })
}

fn menu_bar_style(_theme: &Theme) -> iced::widget::container::Style {
    iced::widget::container::Style {
        background: Some(Background::Color(inputs::SURFACE_BG)),
        border: Border {
            color: inputs::BORDER_COLOR,
            width: 0.0,
            radius: 0.0.into(),
        },
        ..iced::widget::container::Style::default()
    }
}

fn menu_popup_style(_theme: &Theme) -> iced::widget::container::Style {
    iced::widget::container::Style {
        background: Some(Background::Color(inputs::SURFACE_BG)),
        border: Border {
            color: inputs::BORDER_COLOR,
            width: 1.0,
            radius: 6.0.into(),
        },
        shadow: Shadow {
            color: Color::from_rgba8(0, 0, 0, 0.45),
            offset: Vector::new(0.0, 8.0),
            blur_radius: 24.0,
        },
        ..iced::widget::container::Style::default()
    }
}

fn menu_button_style(
    active: bool,
    _theme: &Theme,
    status: iced::widget::button::Status,
) -> iced::widget::button::Style {
    let highlighted = active || matches!(status, iced::widget::button::Status::Hovered);
    iced::widget::button::Style {
        background: highlighted.then_some(Background::Color(inputs::HOVER_BG)),
        text_color: Color::from_rgb8(0xE4, 0xE4, 0xE4),
        border: Border {
            radius: 4.0.into(),
            ..Border::default()
        },
        ..iced::widget::button::Style::default()
    }
}

fn menu_item_style(
    selected: bool,
    _theme: &Theme,
    status: iced::widget::button::Status,
) -> iced::widget::button::Style {
    let highlighted = selected || matches!(status, iced::widget::button::Status::Hovered);
    let disabled = matches!(status, iced::widget::button::Status::Disabled);
    iced::widget::button::Style {
        background: highlighted.then_some(Background::Color(inputs::HOVER_BG)),
        text_color: if disabled {
            Color::from_rgb8(0x7D, 0x7D, 0x7D)
        } else {
            Color::from_rgb8(0xE4, 0xE4, 0xE4)
        },
        border: Border {
            radius: 4.0.into(),
            ..Border::default()
        },
        ..iced::widget::button::Style::default()
    }
}

fn window_menu_popup<'a>(
    definition: &'static WindowMenuDefinition,
    state: &WindowMenuState,
    locale: UiLocale,
    enabled: impl Fn(MenuAction) -> bool + Copy + 'a,
) -> Element<'a, Message> {
    let selected = state.selected_action();
    let mut entries = Column::new().spacing(2);
    for entry in definition.entries {
        match entry {
            Action(action) => {
                let action = *action;
                let is_enabled = enabled(action);
                let shortcut = shortcut_hint(action).unwrap_or_default();
                let content = row![
                    text(translate(locale, action.i18n_key())).size(12),
                    space::horizontal(),
                    text(shortcut)
                        .size(11)
                        .color(Color::from_rgb8(0x9D, 0xA5, 0xB4)),
                ]
                .align_y(Alignment::Center)
                .spacing(16);
                let mut item = keyboard::button(content)
                    .padding([6, 8])
                    .width(Length::Fill)
                    .style(move |theme, status| {
                        menu_item_style(selected == Some(action), theme, status)
                    });
                if is_enabled {
                    item = item.on_press(Message::WindowMenu(WindowMenuEvent::Action(action)));
                }
                entries = entries.push(item);
            }
            Separator => {
                entries = entries.push(
                    container(space::horizontal())
                        .height(Length::Fixed(1.0))
                        .width(Length::Fill)
                        .style(|_| iced::widget::container::Style {
                            background: Some(Background::Color(inputs::BORDER_COLOR)),
                            ..iced::widget::container::Style::default()
                        }),
                );
            }
        }
    }
    let entries = scrollable(entries)
        .direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
        .style(inputs::scrollable_style);
    container(entries)
        .padding(6)
        .width(Length::Fixed(290.0))
        .max_height(520.0)
        .style(menu_popup_style)
        .into()
}

/// Wrap the workspace in the five-group in-window menu used on Linux and Windows.
pub fn window_menu_view<'a>(
    content: Element<'a, Message>,
    state: &WindowMenuState,
    locale: UiLocale,
    enabled: impl Fn(MenuAction) -> bool + Copy + 'a,
) -> Element<'a, Message> {
    let mut groups = row![].spacing(2).align_y(Alignment::Center);
    for definition in WINDOW_MENU_GROUPS {
        let active = state.open_group == Some(definition.group);
        let label = translate(locale, definition.group.i18n_key());
        let label = if state.show_mnemonics {
            format!(
                "{label}  {}",
                definition.group.mnemonic().to_ascii_uppercase()
            )
        } else {
            label
        };
        let trigger = mouse_area(
            keyboard::button(text(label).size(12))
                .padding([5, 8])
                .style(move |theme, status| menu_button_style(active, theme, status))
                .on_press(Message::WindowMenu(WindowMenuEvent::Toggle(
                    definition.group,
                ))),
        )
        .on_enter(Message::WindowMenu(WindowMenuEvent::Hover(
            definition.group,
        )));
        groups = groups.push(
            popover::popover(
                trigger,
                window_menu_popup(definition, state, locale, enabled),
                active,
                Message::WindowMenu(WindowMenuEvent::Dismiss),
            )
            .align_left(),
        );
    }

    let bar = container(groups)
        .padding([5, 6])
        .height(Length::Fixed(34.0))
        .width(Length::Fill)
        .style(menu_bar_style);
    column![bar, content].height(Length::Fill).into()
}

fn window_menu_input(event: iced::keyboard::Event) -> Option<WindowMenuInput> {
    use iced::keyboard::{Key, key};

    match event {
        iced::keyboard::Event::ModifiersChanged(modifiers) => {
            Some(WindowMenuInput::AltChanged(modifiers.alt()))
        }
        iced::keyboard::Event::KeyPressed {
            key: Key::Character(character),
            modifiers,
            ..
        } if modifiers.alt() => character.chars().next().map(WindowMenuInput::Mnemonic),
        iced::keyboard::Event::KeyPressed { key, .. } => match key {
            Key::Named(key::Named::ArrowLeft) => Some(WindowMenuInput::Left),
            Key::Named(key::Named::ArrowRight) => Some(WindowMenuInput::Right),
            Key::Named(key::Named::ArrowDown) => Some(WindowMenuInput::Down),
            Key::Named(key::Named::ArrowUp) => Some(WindowMenuInput::Up),
            Key::Named(key::Named::Home) => Some(WindowMenuInput::Home),
            Key::Named(key::Named::End) => Some(WindowMenuInput::End),
            Key::Named(key::Named::Enter | key::Named::Space) => Some(WindowMenuInput::Activate),
            Key::Named(key::Named::Escape) => Some(WindowMenuInput::Escape),
            _ => None,
        },
        _ => None,
    }
}

/// Keyboard subscription for the non-macOS in-window menu.
pub fn window_menu_subscription() -> Subscription<Message> {
    if cfg!(target_os = "macos") {
        return Subscription::none();
    }
    iced::event::listen_with(|event, _status, _window| match event {
        iced::Event::Keyboard(event) => {
            window_menu_input(event).map(|input| Message::WindowMenu(WindowMenuEvent::Input(input)))
        }
        _ => None,
    })
}

pub(crate) fn action_enabled(
    action: MenuAction,
    has_project: bool,
    active_tab: Option<&TabType>,
    offline: bool,
    interactions_enabled: bool,
) -> bool {
    if !interactions_enabled {
        return !matches!(
            action,
            MenuAction::NewPost
                | MenuAction::ImportMedia
                | MenuAction::Save
                | MenuAction::OpenInBrowser
                | MenuAction::OpenDataFolder
                | MenuAction::Find
                | MenuAction::Replace
                | MenuAction::PublishSelected
                | MenuAction::PreviewPost
                | MenuAction::EditMenu
                | MenuAction::RebuildDatabase
                | MenuAction::ReindexText
                | MenuAction::RebuildEmbeddingIndex
                | MenuAction::FindDuplicates
                | MenuAction::MetadataDiff
                | MenuAction::RegenerateCalendar
                | MenuAction::ValidateTranslations
                | MenuAction::FillMissingTranslations
                | MenuAction::GenerateSitemap
                | MenuAction::ForceRenderSite
                | MenuAction::ValidateSite
                | MenuAction::UploadSite
        );
    }

    let post_tab = active_tab == Some(&TabType::Post);
    let savable_tab = matches!(
        active_tab,
        Some(TabType::Post | TabType::Media | TabType::Templates | TabType::Scripts)
    );
    let text_editor = matches!(
        active_tab,
        Some(TabType::Post | TabType::Templates | TabType::Scripts)
    );
    match action {
        MenuAction::Save => savable_tab,
        MenuAction::Find | MenuAction::Replace => text_editor,
        MenuAction::OpenInBrowser => has_project,
        MenuAction::PublishSelected | MenuAction::PreviewPost => has_project && post_tab,
        MenuAction::UploadSite => has_project && !offline,
        MenuAction::NewPost
        | MenuAction::ImportMedia
        | MenuAction::OpenDataFolder
        | MenuAction::EditMenu
        | MenuAction::RebuildDatabase
        | MenuAction::ReindexText
        | MenuAction::RebuildEmbeddingIndex
        | MenuAction::FindDuplicates
        | MenuAction::MetadataDiff
        | MenuAction::RegenerateCalendar
        | MenuAction::ValidateTranslations
        | MenuAction::FillMissingTranslations
        | MenuAction::GenerateSitemap
        | MenuAction::ForceRenderSite
        | MenuAction::ValidateSite => has_project,
        _ => true,
    }
}

/// Maps between muda `MenuId`s and application `MenuAction`s.
///
/// Also holds clones of the `MenuItem` handles so that labels and
/// enabled state can be changed at runtime (e.g. on locale switch).
pub struct MenuRegistry {
    action_map: HashMap<MenuId, MenuAction>,
    id_map: HashMap<MenuAction, MenuId>,
    items: HashMap<MenuAction, MenuItem>,
}

impl MenuRegistry {
    fn new() -> Self {
        Self {
            action_map: HashMap::new(),
            id_map: HashMap::new(),
            items: HashMap::new(),
        }
    }

    #[cfg(test)]
    pub(crate) fn empty() -> Self {
        Self::new()
    }

    fn register(&mut self, action: MenuAction, item: &MenuItem) {
        self.action_map.insert(item.id().clone(), action);
        self.id_map.insert(action, item.id().clone());
        self.items.insert(action, item.clone());
    }

    /// Look up the `MenuAction` for a raw muda event id.
    pub fn lookup(&self, id: &MenuId) -> Option<MenuAction> {
        self.action_map.get(id).copied()
    }

    /// Enable or disable the menu item for a given action.
    pub fn set_enabled(&self, action: MenuAction, enabled: bool) {
        if let Some(item) = self.items.get(&action) {
            item.set_enabled(enabled);
        }
    }

    /// Change the displayed text for a given action.
    pub fn set_text(&self, action: MenuAction, text: &str) {
        if let Some(item) = self.items.get(&action) {
            item.set_text(text);
        }
    }
}

// ---------------------------------------------------------------------------
// Menu construction helpers
// ---------------------------------------------------------------------------

/// Helper: create a `MenuItem`, register it, and return a reference for
/// appending to a `Submenu`.
fn item(
    registry: &mut MenuRegistry,
    action: MenuAction,
    locale: UiLocale,
    accel: Option<Accelerator>,
) -> MenuItem {
    let label = translate(locale, action.i18n_key());
    let mi = MenuItem::new(label, true, accel);
    registry.register(action, &mi);
    mi
}

/// Build the full native menu bar and a registry that maps ids to actions.
///
/// On macOS this also calls `init_for_nsapp()` to attach the menu.
pub fn build_menu_bar(locale: UiLocale) -> (Menu, MenuRegistry) {
    let menu = Menu::new();
    let mut reg = MenuRegistry::new();

    // -- macOS app menu --
    let app_menu = Submenu::new("bDS", true);
    let _ = app_menu.append(&PredefinedMenuItem::about(None, None));
    let _ = app_menu.append(&PredefinedMenuItem::separator());
    let _ = app_menu.append(&PredefinedMenuItem::services(None));
    let _ = app_menu.append(&PredefinedMenuItem::separator());
    let _ = app_menu.append(&PredefinedMenuItem::hide(None));
    let _ = app_menu.append(&PredefinedMenuItem::hide_others(None));
    let _ = app_menu.append(&PredefinedMenuItem::show_all(None));
    let _ = app_menu.append(&PredefinedMenuItem::separator());
    let _ = app_menu.append(&PredefinedMenuItem::quit(None));

    // -- File --
    let file_menu = Submenu::new(translate(locale, "menu.group.file"), true);
    let _ = file_menu.append(&item(
        &mut reg,
        MenuAction::NewPost,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyN)),
    ));
    let _ = file_menu.append(&item(
        &mut reg,
        MenuAction::ImportMedia,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyI)),
    ));
    let _ = file_menu.append(&item(
        &mut reg,
        MenuAction::Save,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyS)),
    ));
    let _ = file_menu.append(&PredefinedMenuItem::separator());
    let _ = file_menu.append(&item(&mut reg, MenuAction::OpenInBrowser, locale, None));
    let _ = file_menu.append(&item(&mut reg, MenuAction::OpenDataFolder, locale, None));
    let _ = file_menu.append(&PredefinedMenuItem::separator());
    let _ = file_menu.append(&item(&mut reg, MenuAction::ConnectServer, locale, None));
    let _ = file_menu.append(&item(&mut reg, MenuAction::DisconnectServer, locale, None));
    let _ = file_menu.append(&PredefinedMenuItem::separator());
    let _ = file_menu.append(&PredefinedMenuItem::close_window(None));

    // -- Edit --
    let edit_menu = Submenu::new(translate(locale, "menu.group.edit"), true);
    let _ = edit_menu.append(&item(
        &mut reg,
        MenuAction::Undo,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyZ)),
    ));
    let _ = edit_menu.append(&item(
        &mut reg,
        MenuAction::Redo,
        locale,
        Some(Accelerator::new(
            Some(CMD_OR_CTRL | Modifiers::SHIFT),
            Code::KeyZ,
        )),
    ));
    let _ = edit_menu.append(&PredefinedMenuItem::separator());
    let _ = edit_menu.append(&item(
        &mut reg,
        MenuAction::Cut,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyX)),
    ));
    let _ = edit_menu.append(&item(
        &mut reg,
        MenuAction::Copy,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyC)),
    ));
    let _ = edit_menu.append(&item(
        &mut reg,
        MenuAction::Paste,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyV)),
    ));
    let _ = edit_menu.append(&item(
        &mut reg,
        MenuAction::SelectAll,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyA)),
    ));
    let _ = edit_menu.append(&PredefinedMenuItem::separator());
    let _ = edit_menu.append(&item(
        &mut reg,
        MenuAction::Find,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyF)),
    ));
    let _ = edit_menu.append(&item(
        &mut reg,
        MenuAction::Replace,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyH)),
    ));
    let _ = edit_menu.append(&PredefinedMenuItem::separator());
    let _ = edit_menu.append(&item(
        &mut reg,
        MenuAction::EditPreferences,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::Comma)),
    ));

    // -- View --
    let view_menu = Submenu::new(translate(locale, "menu.group.view"), true);
    let _ = view_menu.append(&item(
        &mut reg,
        MenuAction::ViewPosts,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::Digit1)),
    ));
    let _ = view_menu.append(&item(
        &mut reg,
        MenuAction::ViewMedia,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::Digit2)),
    ));
    let _ = view_menu.append(&PredefinedMenuItem::separator());
    let _ = view_menu.append(&item(
        &mut reg,
        MenuAction::ToggleSidebar,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyB)),
    ));
    let _ = view_menu.append(&item(
        &mut reg,
        MenuAction::TogglePanel,
        locale,
        Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyJ)),
    ));
    let _ = view_menu.append(&PredefinedMenuItem::separator());
    let _ = view_menu.append(&PredefinedMenuItem::fullscreen(None));

    // -- Blog --
    let blog_menu = Submenu::new(translate(locale, "menu.group.blog"), true);
    let _ = blog_menu.append(&item(
        &mut reg,
        MenuAction::PublishSelected,
        locale,
        Some(Accelerator::new(
            Some(CMD_OR_CTRL | Modifiers::SHIFT),
            Code::KeyP,
        )),
    ));
    let _ = blog_menu.append(&item(
        &mut reg,
        MenuAction::PreviewPost,
        locale,
        Some(Accelerator::new(
            Some(CMD_OR_CTRL | Modifiers::SHIFT),
            Code::KeyV,
        )),
    ));
    let _ = blog_menu.append(&PredefinedMenuItem::separator());
    let _ = blog_menu.append(&item(&mut reg, MenuAction::EditMenu, locale, None));
    let _ = blog_menu.append(&PredefinedMenuItem::separator());
    let _ = blog_menu.append(&item(&mut reg, MenuAction::RebuildDatabase, locale, None));
    let _ = blog_menu.append(&item(&mut reg, MenuAction::ReindexText, locale, None));
    let _ = blog_menu.append(&item(
        &mut reg,
        MenuAction::RebuildEmbeddingIndex,
        locale,
        None,
    ));
    let _ = blog_menu.append(&item(&mut reg, MenuAction::FindDuplicates, locale, None));
    let _ = blog_menu.append(&item(&mut reg, MenuAction::MetadataDiff, locale, None));
    let _ = blog_menu.append(&PredefinedMenuItem::separator());
    let _ = blog_menu.append(&item(
        &mut reg,
        MenuAction::RegenerateCalendar,
        locale,
        None,
    ));
    let _ = blog_menu.append(&item(
        &mut reg,
        MenuAction::ValidateTranslations,
        locale,
        None,
    ));
    let _ = blog_menu.append(&item(
        &mut reg,
        MenuAction::FillMissingTranslations,
        locale,
        None,
    ));
    let _ = blog_menu.append(&PredefinedMenuItem::separator());
    for action in BLOG_SITE_ACTIONS {
        let accelerator = match action {
            MenuAction::GenerateSitemap => Accelerator::new(Some(CMD_OR_CTRL), Code::KeyR),
            MenuAction::ForceRenderSite => {
                Accelerator::new(Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyR)
            }
            MenuAction::ValidateSite => {
                Accelerator::new(Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyL)
            }
            MenuAction::UploadSite => {
                Accelerator::new(Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyU)
            }
            _ => unreachable!("site action list contains only site actions"),
        };
        let _ = blog_menu.append(&item(&mut reg, action, locale, Some(accelerator)));
    }

    // -- Help --
    let help_menu = Submenu::new(translate(locale, "menu.group.help"), true);
    let _ = help_menu.append(&item(&mut reg, MenuAction::About, locale, None));
    let _ = help_menu.append(&PredefinedMenuItem::separator());
    let _ = help_menu.append(&item(&mut reg, MenuAction::OpenDocumentation, locale, None));
    let _ = help_menu.append(&item(
        &mut reg,
        MenuAction::OpenApiDocumentation,
        locale,
        None,
    ));
    let _ = help_menu.append(&item(
        &mut reg,
        MenuAction::OpenCliDocumentation,
        locale,
        None,
    ));
    let _ = help_menu.append(&item(
        &mut reg,
        MenuAction::OpenMcpDocumentation,
        locale,
        None,
    ));
    let _ = help_menu.append(&item(&mut reg, MenuAction::ViewOnGitHub, locale, None));
    let _ = help_menu.append(&item(&mut reg, MenuAction::ReportIssue, locale, None));

    // Assemble the menu bar
    let _ = menu.append(&app_menu);
    let _ = menu.append(&file_menu);
    let _ = menu.append(&edit_menu);
    let _ = menu.append(&view_menu);
    let _ = menu.append(&blog_menu);
    let _ = menu.append(&help_menu);

    (menu, reg)
}

/// Attach the built menu to the macOS NSApp.
///
/// Must be called **after** the event loop has started (e.g. from the
/// init task or first update), not during `build_menu_bar`.
#[cfg(target_os = "macos")]
pub fn init_menu_for_nsapp(menu: &Menu) {
    menu.init_for_nsapp();
}

/// Re-translate every registered menu item for a new locale.
pub fn update_menu_labels(registry: &MenuRegistry, locale: UiLocale) {
    for &action in MenuAction::ALL {
        registry.set_text(action, &translate(locale, action.i18n_key()));
    }
}

/// Iced subscription that polls muda `MenuEvent`s independently of window input.
///
/// Produces `Message::MenuEvent(MenuId)` so the app can look up the
/// `MenuAction` via its `MenuRegistry`. Native menu selection does not itself
/// produce an Iced window event, so an `iced::event` listener would leave the
/// action queued until the user next moved or clicked inside the window.
pub fn menu_subscription() -> Subscription<Message> {
    iced::time::every(std::time::Duration::from_millis(50)).map(|_| {
        if let Ok(event) = MenuEvent::receiver().try_recv() {
            Message::MenuEvent(event.id)
        } else {
            Message::Noop
        }
    })
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
    use super::*;
    use bds_core::i18n::UiLocale;

    #[test]
    fn window_menu_exposes_the_five_specified_groups() {
        assert_eq!(
            WINDOW_MENU_GROUPS
                .iter()
                .map(|group| group.group)
                .collect::<Vec<_>>(),
            vec![
                WindowMenuGroup::File,
                WindowMenuGroup::Edit,
                WindowMenuGroup::View,
                WindowMenuGroup::Blog,
                WindowMenuGroup::Help,
            ]
        );
        assert!(WINDOW_MENU_GROUPS.iter().all(|group| {
            group
                .entries
                .iter()
                .any(|entry| matches!(entry, WindowMenuEntry::Action(_)))
        }));
        let actions = WINDOW_MENU_GROUPS
            .iter()
            .flat_map(|group| group.entries)
            .filter_map(|entry| match entry {
                WindowMenuEntry::Action(action) => Some(*action),
                WindowMenuEntry::Separator => None,
            })
            .collect::<std::collections::HashSet<_>>();
        assert_eq!(actions.len(), MenuAction::ALL.len());
        assert!(
            MenuAction::ALL
                .iter()
                .all(|action| actions.contains(action))
        );
    }

    #[test]
    fn window_menu_keyboard_navigation_matches_the_layout_spec() {
        let mut state = WindowMenuState::default();

        assert_eq!(state.handle(WindowMenuInput::Mnemonic('f'), |_| true), None);
        assert_eq!(state.open_group, Some(WindowMenuGroup::File));
        assert_eq!(
            state.handle(WindowMenuInput::Down, |action| {
                action != MenuAction::NewPost
            }),
            None
        );
        assert_eq!(state.selected_action(), Some(MenuAction::ImportMedia));
        assert_eq!(
            state.handle(WindowMenuInput::Activate, |_| true),
            Some(MenuAction::ImportMedia)
        );
        assert_eq!(state.open_group, None);

        state.handle(WindowMenuInput::Mnemonic('b'), |_| true);
        state.handle(WindowMenuInput::Right, |_| true);
        assert_eq!(state.open_group, Some(WindowMenuGroup::Help));
        state.handle(WindowMenuInput::Escape, |_| true);
        assert_eq!(state.open_group, None);
    }

    #[test]
    fn window_menu_alt_state_controls_mnemonic_hints() {
        let mut state = WindowMenuState::default();

        state.handle(WindowMenuInput::AltChanged(true), |_| true);
        assert!(state.show_mnemonics);
        state.handle(WindowMenuInput::AltChanged(false), |_| true);
        assert!(!state.show_mnemonics);
    }

    #[test]
    fn i18n_keys_resolve_for_english() {
        for &action in MenuAction::ALL {
            let key = action.i18n_key();
            let label = translate(UiLocale::En, key);
            assert_ne!(label, key, "missing English translation for {key}");
        }
    }

    #[test]
    fn registry_register_and_lookup_roundtrip() {
        let mut reg = MenuRegistry::new();
        let mi = MenuItem::new("Test", true, None);
        reg.register(MenuAction::Save, &mi);

        assert_eq!(reg.lookup(mi.id()), Some(MenuAction::Save));
    }

    #[test]
    fn registry_lookup_missing_returns_none() {
        let reg = MenuRegistry::new();
        let bogus = MenuId::new("nonexistent");
        assert_eq!(reg.lookup(&bogus), None);
    }

    #[test]
    fn registry_set_enabled_and_text() {
        let mut reg = MenuRegistry::new();
        let mi = MenuItem::new("Original", true, None);
        reg.register(MenuAction::NewPost, &mi);

        reg.set_enabled(MenuAction::NewPost, false);
        assert!(!mi.is_enabled());

        reg.set_text(MenuAction::NewPost, "Changed");
        assert_eq!(mi.text(), "Changed");
    }

    #[test]
    fn all_actions_have_unique_i18n_keys() {
        let mut seen = std::collections::HashSet::new();
        for &action in MenuAction::ALL {
            let key = action.i18n_key();
            assert!(seen.insert(key), "duplicate i18n key: {key}");
        }
    }

    #[test]
    fn force_render_script_action_is_distinct_from_normal_render() {
        assert_eq!(
            MenuAction::from_script_name("force_render_site"),
            Some(MenuAction::ForceRenderSite)
        );
    }

    #[test]
    fn force_render_is_immediately_below_normal_render_in_the_blog_menu() {
        let normal = BLOG_SITE_ACTIONS
            .iter()
            .position(|action| *action == MenuAction::GenerateSitemap)
            .unwrap();

        assert_eq!(BLOG_SITE_ACTIONS[normal + 1], MenuAction::ForceRenderSite);
    }

    #[test]
    fn enabled_state_follows_application_rules() {
        assert!(action_enabled(
            MenuAction::Save,
            true,
            Some(&TabType::Media),
            false,
            true
        ));
        assert!(!action_enabled(
            MenuAction::Find,
            true,
            Some(&TabType::Media),
            false,
            true
        ));
        assert!(action_enabled(
            MenuAction::FillMissingTranslations,
            true,
            None,
            true,
            true
        ));
        assert!(!action_enabled(
            MenuAction::UploadSite,
            true,
            None,
            true,
            true
        ));
        assert!(action_enabled(
            MenuAction::OpenInBrowser,
            true,
            None,
            false,
            true
        ));
        assert!(!action_enabled(
            MenuAction::OpenInBrowser,
            false,
            Some(&TabType::Post),
            false,
            true
        ));
        assert!(!action_enabled(
            MenuAction::ReindexText,
            true,
            None,
            false,
            false
        ));
        assert!(action_enabled(MenuAction::About, true, None, false, false));
    }
}
