use std::collections::HashMap;

use axum::extract::FromRef;

use crate::app_state::AppState;

pub const NAVBAR_ITEM_TEAMS: &str = "teams";
pub const NAVBAR_ITEM_PROJECTS: &str = "projects";
pub const NAVBAR_ITEM_CHANNELS: &str = "channels";

#[derive(Clone, Debug)]
pub struct BreadcrumbTrail {
    base_path: String,
    breadcrumbs: Vec<Breadcrumb>,
}

impl BreadcrumbTrail {
    /// Initialize with a non-empty base path.
    pub fn from_base_path(base_path: &str) -> Self {
        Self {
            base_path: base_path.to_owned(),
            breadcrumbs: Vec::new(),
        }
    }

    /// Append an i18n path segment to the base path.
    pub fn with_i18n_slug(mut self, language_code: &str) -> Self {
        self.base_path.push('/');
        self.base_path.push_str(language_code);
        self
    }

    /// Add a breadcrumb by name and slug. If other breadcrumbs have already
    /// been added, href will be generated by appending it to the previous href
    /// as "<previous>/<slug>". Otherwise, it will be appended to the base path
    /// with i18n slug (if any).
    pub fn push_slug(mut self, label: &str, slug: &str) -> Self {
        let href = if let Some(prev_breadcrumb) = self.iter().last() {
            format!(
                "{}/{}",
                prev_breadcrumb.href,
                percent_encoding::percent_encode(
                    slug.as_bytes(),
                    percent_encoding::NON_ALPHANUMERIC
                )
            )
        } else {
            format!("{}/{}", self.base_path, slug)
        };
        self.breadcrumbs.push(Breadcrumb {
            label: label.to_owned(),
            href,
        });
        self
    }

    pub fn iter(&self) -> std::slice::Iter<'_, Breadcrumb> {
        self.breadcrumbs.iter()
    }

    /// Get an absolute URI path, starting from the child of the last
    /// breadcrumb. For example, if the last breadcrumb has an href of
    /// "/en/teams/team123" and the relative path is "../team456", the result
    /// will be "/en/teams/team456". If no breadcrumbs exist, the base path
    /// with i18n slug (if any) will be used.
    pub fn join(&self, rel_path: &str) -> String {
        let base = if let Some(breadcrumb) = self.iter().last() {
            &breadcrumb.href
        } else {
            &self.base_path
        };
        let mut path_buf: Vec<&str> = base.split('/').collect();
        for rel_segment in rel_path.split('/') {
            if rel_segment == "." {
                continue;
            } else if rel_segment == ".." {
                path_buf.pop();
            } else {
                path_buf.push(rel_segment);
            }
        }
        path_buf.join("/")
    }
}

impl IntoIterator for BreadcrumbTrail {
    type Item = Breadcrumb;
    type IntoIter = std::vec::IntoIter<Breadcrumb>;

    fn into_iter(self) -> Self::IntoIter {
        self.breadcrumbs.into_iter()
    }
}

#[derive(Clone, Debug)]
pub struct Breadcrumb {
    pub href: String,
    pub label: String,
}

#[derive(Clone, Debug)]
pub struct NavbarBuilder {
    base_path: String,
    items: Vec<NavbarItem>,
    active_item: Option<String>,
    params: HashMap<String, String>,
}

impl NavbarBuilder {
    pub fn new() -> Self {
        Self {
            base_path: "".to_owned(),
            items: Vec::new(),
            active_item: None,
            params: HashMap::new(),
        }
    }

    pub fn with_base_path(mut self, base_path: &str) -> Self {
        self.base_path = base_path.to_owned();
        self
    }

    /// Add a navbar item. Subpath is a path relative to the base path, and it
    /// may contain placeholders for path params, such as "/{lang}/teams".
    /// The navbar item will only be displayed if all corresponding path params
    /// are registered using .with_param().
    pub fn push_item(mut self, id: &str, label: &str, subpath: &str) -> Self {
        self.items.push(NavbarItem {
            id: id.to_owned(),
            href: subpath.to_owned(),
            label: label.to_owned(),
        });
        self
    }

    /// Registers a path param with the navbar builder.
    pub fn with_param(mut self, k: &str, v: &str) -> Self {
        self.params.insert(k.to_owned(), v.to_owned());
        self
    }

    /// If a visible navbar item matches the provided ID, it will render as
    /// active. Calling this method overrides any previously specified value.
    pub fn with_active_item(mut self, item_id: &str) -> Self {
        self.active_item = Some(item_id.to_owned());
        self
    }

    pub fn build(self) -> Navbar {
        let mut built_items: Vec<NavbarItem> = Vec::with_capacity(self.items.len());
        for item in self.items {
            let path_segments = item.href.split('/');
            let substituted_segments: Vec<Option<&str>> = path_segments
                .map(|segment| {
                    if segment.starts_with("{") && segment.ends_with("}") {
                        let param_k = segment[1..segment.len() - 1].trim();
                        self.params.get(param_k).map(|v| v.as_str())
                    } else {
                        Some(segment)
                    }
                })
                .collect();
            if substituted_segments.iter().all(|segment| segment.is_some()) {
                built_items.push(NavbarItem {
                    id: item.id,
                    href: format!(
                        "{}{}",
                        self.base_path,
                        substituted_segments
                            .into_iter()
                            .map(|segment| {
                                segment.expect(
                                    "should already have checked that all path segments are Some",
                                )
                            })
                            .collect::<Vec<_>>()
                            .join("/")
                    ),
                    label: item.label,
                });
            }
        }
        Navbar {
            active_item: self.active_item,
            items: built_items,
        }
    }
}

impl Default for NavbarBuilder {
    fn default() -> Self {
        Self::new()
            .push_item(NAVBAR_ITEM_TEAMS, "Teams", "/en/teams")
            .push_item(
                NAVBAR_ITEM_PROJECTS,
                "Projects",
                "/en/teams/{team_id}/projects",
            )
            .push_item(
                NAVBAR_ITEM_CHANNELS,
                "Channels",
                "/en/teams/{team_id}/channels",
            )
    }
}

impl<S> FromRef<S> for NavbarBuilder
where
    S: Into<AppState> + Clone,
{
    fn from_ref(state: &S) -> Self {
        Into::<AppState>::into(state.clone())
            .navbar_template
            .clone()
    }
}

#[derive(Clone, Debug)]
pub struct Navbar {
    pub items: Vec<NavbarItem>,
    pub active_item: Option<String>,
}

#[derive(Clone, Debug)]
pub struct NavbarItem {
    pub href: String,
    pub id: String,
    pub label: String,
}
