/// The kind of content a tab holds.
///
/// 17 tab types per tabs.allium spec. Each maps 1:1 to an editor route.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TabType {
    Post,
    Media,
    Settings,
    Style,
    Tags,
    Chat,
    Import,
    MenuEditor,
    MetadataDiff,
    GitDiff,
    Scripts,
    Templates,
    Documentation,
    ApiDocumentation,
    CliDocumentation,
    McpDocumentation,
    SiteValidation,
    TranslationValidation,
    FindDuplicates,
}

impl TabType {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Post => "post",
            Self::Media => "media",
            Self::Settings => "settings",
            Self::Style => "style",
            Self::Tags => "tags",
            Self::Chat => "chat",
            Self::Import => "import",
            Self::MenuEditor => "menu_editor",
            Self::MetadataDiff => "metadata_diff",
            Self::GitDiff => "git_diff",
            Self::Scripts => "scripts",
            Self::Templates => "templates",
            Self::Documentation => "documentation",
            Self::ApiDocumentation => "api_documentation",
            Self::CliDocumentation => "cli_documentation",
            Self::McpDocumentation => "mcp_documentation",
            Self::SiteValidation => "site_validation",
            Self::TranslationValidation => "translation_validation",
            Self::FindDuplicates => "find_duplicates",
        }
    }

    pub fn from_persisted(value: &str) -> Option<Self> {
        Some(match value {
            "post" => Self::Post,
            "media" => Self::Media,
            "settings" => Self::Settings,
            "style" => Self::Style,
            "tags" => Self::Tags,
            "chat" => Self::Chat,
            "import" => Self::Import,
            "menu_editor" => Self::MenuEditor,
            "metadata_diff" => Self::MetadataDiff,
            "git_diff" => Self::GitDiff,
            "scripts" => Self::Scripts,
            "templates" => Self::Templates,
            "documentation" => Self::Documentation,
            "api_documentation" => Self::ApiDocumentation,
            "cli_documentation" => Self::CliDocumentation,
            "mcp_documentation" => Self::McpDocumentation,
            "site_validation" => Self::SiteValidation,
            "translation_validation" => Self::TranslationValidation,
            "find_duplicates" => Self::FindDuplicates,
            _ => return None,
        })
    }

    /// Singleton tool tabs per tabs.allium: always one instance, id = type name.
    ///
    /// Singleton types (12): settings, tags, style, menu_editor,
    /// documentation, api_documentation, cli_documentation, mcp_documentation,
    /// metadata_diff, site_validation, translation_validation, find_duplicates.
    ///
    /// Entity types (keyed by external ID): post, media, scripts (keyed),
    /// templates (keyed), chat, import, git_diff.
    pub fn is_singleton(&self) -> bool {
        matches!(
            self,
            Self::Settings
                | Self::Style
                | Self::Tags
                | Self::MenuEditor
                | Self::MetadataDiff
                | Self::Documentation
                | Self::ApiDocumentation
                | Self::CliDocumentation
                | Self::McpDocumentation
                | Self::SiteValidation
                | Self::TranslationValidation
                | Self::FindDuplicates
        )
    }

    /// Return the canonical string ID used for singleton tabs.
    pub fn singleton_id(&self) -> &'static str {
        match self {
            Self::Settings => "settings",
            Self::Style => "style",
            Self::Tags => "tags",
            Self::MenuEditor => "menu_editor",
            Self::MetadataDiff => "metadata_diff",
            Self::Documentation => "documentation",
            Self::ApiDocumentation => "api_documentation",
            Self::CliDocumentation => "cli_documentation",
            Self::McpDocumentation => "mcp_documentation",
            Self::SiteValidation => "site_validation",
            Self::TranslationValidation => "translation_validation",
            Self::FindDuplicates => "find_duplicates",
            // Entity types don't have a singleton ID — callers should use external IDs
            _ => "",
        }
    }

    /// Return the i18n key for the tab title of a singleton tab type.
    /// Per tabs.allium: singletons use i18n key lookup.
    pub fn i18n_key(&self) -> Option<&'static str> {
        match self {
            Self::Settings => Some("common.settings"),
            Self::Style => Some("tabBar.style"),
            Self::Tags => Some("tabBar.tags"),
            Self::MenuEditor => Some("tabBar.menuEditor"),
            Self::MetadataDiff => Some("tabBar.metadataDiff"),
            Self::Documentation => Some("tabBar.documentation"),
            Self::ApiDocumentation => Some("tabBar.apiDocumentation"),
            Self::CliDocumentation => Some("tabBar.cliDocumentation"),
            Self::McpDocumentation => Some("tabBar.mcpDocumentation"),
            Self::SiteValidation => Some("tabBar.siteValidation"),
            Self::TranslationValidation => Some("tabBar.translationValidation"),
            Self::FindDuplicates => Some("tabBar.findDuplicates"),
            _ => None,
        }
    }
}

/// A single tab in the editor area.
#[derive(Debug, Clone)]
pub struct Tab {
    pub id: String,
    pub tab_type: TabType,
    pub title: String,
    pub is_transient: bool,
    /// Only post tabs show dirty state (content differs from saved).
    pub is_dirty: bool,
}

/// Open (or focus) a tab in the tab list.
///
/// Per tabs.allium `OpenTab` rule:
///
/// * **Dedup** — if a tab with same `(type, id)` already exists, activate it.
///   If the intent was `pin`, also set `is_transient = false`.
/// * **Singleton** — if a tab of the same singleton type exists, return its
///   index (singletons have id == type name, so dedup covers this).
/// * **Transient replacement** — if opening as transient and a transient tab
///   of same type exists, replace it with the new tab.
/// * Otherwise — append a new tab.
///
/// Returns the index of the resulting tab.
pub fn open_tab(tabs: &mut Vec<Tab>, new_tab: Tab) -> usize {
    // Dedup: exact (type, id) match → reuse
    if let Some(idx) = tabs
        .iter()
        .position(|t| t.tab_type == new_tab.tab_type && t.id == new_tab.id)
    {
        // If intent is pin, upgrade to permanent
        if !new_tab.is_transient {
            tabs[idx].is_transient = false;
        }
        return idx;
    }

    // Singleton: only one instance allowed (catches id mismatch edge cases)
    if new_tab.tab_type.is_singleton()
        && let Some(idx) = tabs.iter().position(|t| t.tab_type == new_tab.tab_type)
    {
        return idx;
    }

    // Transient replacement: replace existing transient of same type
    if new_tab.is_transient
        && let Some(idx) = tabs
            .iter()
            .position(|t| t.tab_type == new_tab.tab_type && t.is_transient)
    {
        tabs[idx] = new_tab;
        return idx;
    }

    tabs.push(new_tab);
    tabs.len() - 1
}

/// Remove the tab whose `id` matches and return the index that should be
/// selected next.
///
/// Prefers the same index (now occupied by the next tab), clamped to
/// `len - 1`.  Returns `None` when the list is empty after removal.
pub fn close_tab(tabs: &mut Vec<Tab>, id: &str) -> Option<usize> {
    let pos = tabs.iter().position(|t| t.id == id)?;
    tabs.remove(pos);
    if tabs.is_empty() {
        None
    } else {
        Some(pos.min(tabs.len() - 1))
    }
}

/// Mark a transient tab as permanent (non-transient).
pub fn pin_tab(tabs: &mut [Tab], id: &str) {
    if let Some(tab) = tabs.iter_mut().find(|t| t.id == id) {
        tab.is_transient = false;
    }
}

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

    fn make_tab(id: &str, tab_type: TabType, transient: bool) -> Tab {
        Tab {
            id: id.to_string(),
            tab_type,
            title: id.to_string(),
            is_transient: transient,
            is_dirty: false,
        }
    }

    #[test]
    fn singleton_dedup() {
        let mut tabs = Vec::new();
        let idx1 = open_tab(&mut tabs, make_tab("settings", TabType::Settings, false));
        let idx2 = open_tab(&mut tabs, make_tab("settings", TabType::Settings, false));
        assert_eq!(idx1, idx2);
        assert_eq!(tabs.len(), 1);
        assert_eq!(tabs[0].id, "settings");
    }

    #[test]
    fn transient_replacement() {
        let mut tabs = Vec::new();
        open_tab(&mut tabs, make_tab("p1", TabType::Post, true));
        assert_eq!(tabs.len(), 1);
        let idx = open_tab(&mut tabs, make_tab("p2", TabType::Post, true));
        assert_eq!(tabs.len(), 1);
        assert_eq!(tabs[idx].id, "p2");
    }

    #[test]
    fn pin_makes_permanent() {
        let mut tabs = Vec::new();
        open_tab(&mut tabs, make_tab("p1", TabType::Post, true));
        assert!(tabs[0].is_transient);
        pin_tab(&mut tabs, "p1");
        assert!(!tabs[0].is_transient);
    }

    #[test]
    fn close_selects_neighbor() {
        let mut tabs = Vec::new();
        open_tab(&mut tabs, make_tab("a", TabType::Post, false));
        open_tab(&mut tabs, make_tab("b", TabType::Media, false));
        open_tab(&mut tabs, make_tab("c", TabType::Post, false));

        // Close the middle tab.
        let next = close_tab(&mut tabs, "b");
        assert_eq!(next, Some(1)); // index 1 now holds "c"
        assert_eq!(tabs[1].id, "c");

        // Close the last tab.
        let next = close_tab(&mut tabs, "c");
        assert_eq!(next, Some(0)); // clamped to len-1

        // Close the only remaining tab.
        let next = close_tab(&mut tabs, "a");
        assert_eq!(next, None);
    }

    #[test]
    fn open_appends_non_singleton_non_transient() {
        let mut tabs = Vec::new();
        let i0 = open_tab(&mut tabs, make_tab("p1", TabType::Post, false));
        let i1 = open_tab(&mut tabs, make_tab("p2", TabType::Post, false));
        assert_eq!(i0, 0);
        assert_eq!(i1, 1);
        assert_eq!(tabs.len(), 2);
    }

    #[test]
    fn entity_dedup_by_type_and_id() {
        // Re-opening the same (type, id) should not create a new tab.
        let mut tabs = Vec::new();
        open_tab(&mut tabs, make_tab("s1", TabType::Scripts, true));
        open_tab(&mut tabs, make_tab("s2", TabType::Scripts, true));
        // s2 replaced s1 (transient replacement)
        assert_eq!(tabs.len(), 1);
        assert_eq!(tabs[0].id, "s2");

        // Re-open s2 — dedup should reuse
        let idx = open_tab(&mut tabs, make_tab("s2", TabType::Scripts, false));
        assert_eq!(tabs.len(), 1);
        assert_eq!(idx, 0);
        // Pin intent should upgrade transient to permanent
        assert!(!tabs[0].is_transient);
    }

    #[test]
    fn scripts_not_singleton() {
        // Script tabs are entity tabs (keyed by scriptId), not singletons
        let mut tabs = Vec::new();
        open_tab(&mut tabs, make_tab("s1", TabType::Scripts, false));
        open_tab(&mut tabs, make_tab("s2", TabType::Scripts, false));
        assert_eq!(tabs.len(), 2);
    }

    #[test]
    fn templates_not_singleton() {
        let mut tabs = Vec::new();
        open_tab(&mut tabs, make_tab("t1", TabType::Templates, false));
        open_tab(&mut tabs, make_tab("t2", TabType::Templates, false));
        assert_eq!(tabs.len(), 2);
    }

    #[test]
    fn chat_not_singleton() {
        let mut tabs = Vec::new();
        open_tab(&mut tabs, make_tab("c1", TabType::Chat, false));
        open_tab(&mut tabs, make_tab("c2", TabType::Chat, false));
        assert_eq!(tabs.len(), 2);
    }

    #[test]
    fn persisted_tab_type_names_round_trip() {
        for tab_type in [
            TabType::Post,
            TabType::Media,
            TabType::Settings,
            TabType::Style,
            TabType::Tags,
            TabType::Chat,
            TabType::Import,
            TabType::MenuEditor,
            TabType::MetadataDiff,
            TabType::GitDiff,
            TabType::Scripts,
            TabType::Templates,
            TabType::Documentation,
            TabType::ApiDocumentation,
            TabType::CliDocumentation,
            TabType::McpDocumentation,
            TabType::SiteValidation,
            TabType::TranslationValidation,
            TabType::FindDuplicates,
        ] {
            assert_eq!(TabType::from_persisted(tab_type.as_str()), Some(tab_type));
        }
        assert_eq!(TabType::from_persisted("unknown"), None);
    }
}
