use maud::{Markup, Render, html};

/// The navigation: the ways in for a visitor, and the way back to oneself
/// and out again for someone signed in.
pub(super) struct Nav<'a> {
    pub(super) viewer: Option<&'a str>,
}

impl Render for Nav<'_> {
    fn render(&self) -> Markup {
        let links = match self.viewer {
            Some(name) => html! {
                (FrogueUsername(name))
                form class="session" method="post" action="/logout" {
                    button type="submit" { "Log out" }
                }
            },
            None => html! {
                a href="/login" { "Log in" }
                a href="/register" { "Register" }
            },
        };

        html! { nav { (links) } }
    }
}

/// An error message, rendered only when there is one.
pub(super) struct Complaint<'a>(pub(super) Option<&'a str>);

impl Render for Complaint<'_> {
    fn render(&self) -> Markup {
        html! {
            @if let Some(message) = self.0 {
                p class="error" { (message) }
            }
        }
    }
}

/// A repository heading: the owner, a slash, the slug, and the visibility
/// indicator.
pub(super) struct RepositoryHeading<'a> {
    pub(super) owner: &'a str,
    pub(super) slug: &'a str,
    pub(super) visibility: &'a str,
}

impl Render for RepositoryHeading<'_> {
    fn render(&self) -> Markup {
        html! {
            h2 {
                (FrogueUsername(self.owner)) "/" (self.slug)
                " "
                (FrogueRepositoryVisibility(self.visibility))
            }
        }
    }
}

/// A text input field within a label.
pub(super) struct TextField<'a> {
    pub(super) label: Option<&'a str>,
    pub(super) name: &'a str,
    pub(super) input_type: &'a str,
    pub(super) value: Option<&'a str>,
    pub(super) required: bool,
    pub(super) maxlength: u32,
}

impl Render for TextField<'_> {
    fn render(&self) -> Markup {
        html! {
            label {
                @if let Some(label_text) = self.label {
                    (label_text)
                }
                input type=(self.input_type) name=(self.name) required[self.required]
                    value=(self.value.unwrap_or("")) maxlength=(self.maxlength);
            }
        }
    }
}

/// A password input field within a label.
///
/// Password fields have no value attribute, so the browser never pre-fills
/// them from prior submissions.
pub(super) struct PasswordField<'a> {
    pub(super) label: &'a str,
    pub(super) name: &'a str,
    pub(super) required: bool,
}

impl Render for PasswordField<'_> {
    fn render(&self) -> Markup {
        html! {
            label {
                (self.label)
                input type="password" name=(self.name) required[self.required];
            }
        }
    }
}

/// A textarea field within a label.
pub(super) struct TextArea<'a> {
    pub(super) label: Option<&'a str>,
    pub(super) name: &'a str,
    pub(super) rows: u32,
    pub(super) value: Option<&'a str>,
    pub(super) required: bool,
}

impl Render for TextArea<'_> {
    fn render(&self) -> Markup {
        html! {
            label {
                @if let Some(label_text) = self.label {
                    (label_text)
                }
                textarea name=(self.name) rows=(self.rows) required[self.required] {
                    (self.value.unwrap_or(""))
                }
            }
        }
    }
}

/// A frogue username, with a link to the user and prefixed by '~'
pub(super) struct FrogueUsername<'a>(pub(super) &'a str);

impl Render for FrogueUsername<'_> {
    fn render(&self) -> Markup {
        let username = self.0;
        html! {
            a class="frogue-username" href={"/~" (username)} {"~" (username)}
        }
    }
}

/// A comment author, local or remote: local gets a `~username` link, remote
/// gets a link to their actor URL plus a badge saying so.
pub(super) struct AuthorName<'a>(pub(super) &'a crate::comments::CommentAuthor);

impl Render for AuthorName<'_> {
    fn render(&self) -> Markup {
        let author = self.0;
        if author.is_remote {
            html! {
                @if let Some(url) = &author.actor_url {
                    a href=(url) { (author.name) }
                } @else {
                    (author.name)
                }
                " "
                span class="remote-badge" { "remote" }
            }
        } else {
            html! { (FrogueUsername(&author.name)) }
        }
    }
}

/// A frogue repository visibility indicator
pub(super) struct FrogueRepositoryVisibility<'a>(pub(super) &'a str);

impl Render for FrogueRepositoryVisibility<'_> {
    fn render(&self) -> Markup {
        html! {
            @if self.0 == "private" {
                dfn title="private" class="visibility" {
                    "🔒"
                }
            }
        }
    }
}

/// Which of a repository's pages is on show, for the tab bar that says so.
///
/// A tab only exists once the page behind it does: there is no value in a
/// tab that leads nowhere.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum RepositoryTab {
    Summary,
    Tickets,
    Patches,
    Refs,
    Commits,
    Compare,
    Browse,
    Settings,
}

/// The navigation across a repository's own pages, with the current one
/// marked so a visitor can tell where they are in the repository.
pub(super) struct RepoNav<'a> {
    pub(super) owner: &'a str,
    pub(super) slug: &'a str,
    /// The branch the browse tab addresses: the tree page needs a default,
    /// and the repository opens on it.
    pub(super) default_branch: &'a str,
    pub(super) current: RepositoryTab,
    pub(super) is_owner: bool,
}

impl Render for RepoNav<'_> {
    fn render(&self) -> Markup {
        let base = format!("/~{}/{}", self.owner, self.slug);
        html! {
            nav class="repository-tabs" {
                (repository_tab(&base, "Summary", self.current == RepositoryTab::Summary))
                (repository_tab(
                    &format!("{base}/tickets"),
                    "Tickets",
                    self.current == RepositoryTab::Tickets,
                ))
                (repository_tab(
                    &format!("{base}/patches"),
                    "Patches",
                    self.current == RepositoryTab::Patches,
                ))
                (repository_tab(&format!("{base}/refs"), "Refs", self.current == RepositoryTab::Refs))
                (repository_tab(
                    &format!("{base}/commits/{}", self.default_branch),
                    "Commits",
                    self.current == RepositoryTab::Commits,
                ))
                (repository_tab(
                    &format!("{base}/compare"),
                    "Compare",
                    self.current == RepositoryTab::Compare,
                ))
                (repository_tab(
                    &format!("{base}/tree/{}", self.default_branch),
                    "Browse",
                    self.current == RepositoryTab::Browse,
                ))
                @if self.is_owner {
                    (repository_tab(
                        &format!("{base}/settings"),
                        "Settings",
                        self.current == RepositoryTab::Settings,
                    ))
                }
            }
        }
    }
}

/// A repository's full header: the heading with owner/slug/visibility,
/// followed by the navigation tabs.
///
/// This combines RepositoryHeading and RepoNav for pages that need both,
/// which is most repository pages.
pub(super) struct RepositoryChrome<'a> {
    pub(super) owner: &'a str,
    pub(super) slug: &'a str,
    pub(super) visibility: &'a str,
    pub(super) default_branch: &'a str,
    pub(super) current_tab: RepositoryTab,
    pub(super) is_owner: bool,
}

impl Render for RepositoryChrome<'_> {
    fn render(&self) -> Markup {
        html! {
            (RepositoryHeading {
                owner: self.owner,
                slug: self.slug,
                visibility: self.visibility,
            })
            (RepoNav {
                owner: self.owner,
                slug: self.slug,
                default_branch: self.default_branch,
                current: self.current_tab,
                is_owner: self.is_owner,
            })
        }
    }
}

/// A single-button POST form: one click to perform an action.
///
/// Covers every place the UI needs a one-shot act: delete a thing, unfollow,
/// react, boost, merge. The variant picks the button's tone; `confirm` wraps
/// it in a `<details>` so destructive acts need a second deliberate click,
/// with no JS involved.
pub(super) struct ActionButton<'a> {
    pub(super) action: String,
    pub(super) label: &'a str,
    pub(super) variant: ActionVariant,
    pub(super) hidden: &'a [(&'a str, &'a str)],
    pub(super) confirm: Option<&'a str>,
}

pub(super) enum ActionVariant {
    /// Neutral bordered button: reactions, unfollow.
    Quiet,
    /// Green-tinted "on" state: already reacted/boosted.
    Active,
    /// Red destructive button: delete.
    Danger,
    /// The standard green submit: merge.
    Solid,
}

impl Render for ActionButton<'_> {
    fn render(&self) -> Markup {
        let variant_class = match self.variant {
            ActionVariant::Quiet => "quiet",
            ActionVariant::Active => "active",
            ActionVariant::Danger => "danger",
            ActionVariant::Solid => "solid",
        };

        let button = html! {
            button type="submit" class=(format!("action-button {}", variant_class)) { (self.label) }
        };

        let hidden_inputs = html! {
            @for (name, value) in self.hidden {
                input type="hidden" name=(name) value=(value);
            }
        };

        match self.confirm {
            Some(msg) => html! {
                form method="post" class="action-form" action=(&self.action) {
                    (hidden_inputs)
                    details class="confirm" {
                        summary { (self.label) }
                        p { (msg) }
                        (button)
                    }
                }
            },
            None => html! {
                form method="post" class="action-form" action=(&self.action) {
                    (hidden_inputs)
                    (button)
                }
            },
        }
    }
}

/// One tab of a repository's navigation.
///
/// `aria-current` is written only when it is true: an attribute present but
/// empty says nothing, and a screen reader would pass over it. The
/// alternative - a conditional attribute in maud - can only carry the
/// attribute's own name as its value, which is not a value `aria-current`
/// understands.
pub(super) fn repository_tab(href: &str, label: &str, current: bool) -> Markup {
    if current {
        html! { a href=(href) aria-current="page" { (label) } }
    } else {
        html! { a href=(href) { (label) } }
    }
}

/// A reference selection dropdown with branches, tags, and patches.
pub(super) struct RefSelect<'a> {
    pub(super) name: &'a str,
    pub(super) label: &'a str,
    pub(super) branches: &'a [super::repositories::RefSummary],
    pub(super) tags: &'a [super::repositories::RefSummary],
    pub(super) patches: &'a [super::compare::PatchSummary],
    /// The value to mark as selected. If it matches no listed option, a
    /// synthetic option is added at the top so the dropdown reflects the
    /// current state rather than silently falling back to the first branch.
    pub(super) selected: &'a str,
}

impl Render for RefSelect<'_> {
    fn render(&self) -> Markup {
        let listed = self.branches.iter().any(|b| b.name == self.selected)
            || self.tags.iter().any(|t| t.name == self.selected)
            || self
                .patches
                .iter()
                .any(|p| format!("patches/{}", p.number) == self.selected);

        html! {
            label {
                (self.label)
                select name=(self.name) {
                    @if !listed && !self.selected.is_empty() {
                        option value=(self.selected) selected { (self.selected) }
                    }
                    @if !self.branches.is_empty() {
                        optgroup label="Branches" {
                            @for branch in self.branches {
                                option value=(branch.name) selected[branch.name == self.selected] {
                                    (branch.name)
                                }
                            }
                        }
                    }
                    @if !self.tags.is_empty() {
                        optgroup label="Tags" {
                            @for tag in self.tags {
                                option value=(tag.name) selected[tag.name == self.selected] {
                                    (tag.name)
                                }
                            }
                        }
                    }
                    @if !self.patches.is_empty() {
                        optgroup label="Patches" {
                            @for patch in self.patches {
                                option value=(format!("patches/{}", patch.number)) selected[format!("patches/{}", patch.number) == self.selected] {
                                    "#" (patch.number) " " (patch.summary)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

/// A filter link that can be marked as selected, for ticket/patch filters.
///
/// When selected, adds a "selected" class and aria-current="true".
pub(super) struct FilterLink<'a> {
    pub(super) href: &'a str,
    pub(super) label: &'a str,
    pub(super) base_class: &'a str,
    pub(super) selected: bool,
}

impl Render for FilterLink<'_> {
    fn render(&self) -> Markup {
        if self.selected {
            html! {
                a class=(format!("{} selected", self.base_class)) href=(self.href) aria-current="true" { (self.label) }
            }
        } else {
            html! { a class=(self.base_class) href=(self.href) { (self.label) } }
        }
    }
}

/// A reusable pagination component.
///
/// Renders Previous/Next links based on the current page and whether more items
/// exist. The base_url should include the path but not the query string.
pub(super) struct Pagination {
    pub current_page: u64,
    pub has_next: bool,
    pub base_url: String,
}

impl Render for Pagination {
    fn render(&self) -> Markup {
        html! {
            nav class="pagination" {
                @if self.current_page > 0 {
                    a href=(format!("{}?page={}", self.base_url, self.current_page - 1)) class="pagination-prev" { "← Previous" }
                }
                @if self.has_next {
                    a href=(format!("{}?page={}", self.base_url, self.current_page + 1)) class="pagination-next" { "Next →" }
                }
            }
        }
    }
}

pub struct DateTime {
    dt: chrono::DateTime<chrono::FixedOffset>,
}

impl From<&chrono::DateTime<chrono::FixedOffset>> for DateTime {
    fn from(value: &chrono::DateTime<chrono::FixedOffset>) -> Self {
        Self { dt: value.clone() }
    }
}

impl From<&chrono::DateTime<chrono::Utc>> for DateTime {
    fn from(value: &chrono::DateTime<chrono::Utc>) -> Self {
        Self {
            dt: value.fixed_offset(),
        }
    }
}

impl Render for DateTime {
    fn render(&self) -> Markup {
        html! {
            time datetime=(self.dt.to_rfc3339()) { (self.dt.to_rfc3339()) }
        }
    }
}

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

    #[test]
    fn action_button_renders_basic_form() {
        let button = ActionButton {
            action: "/delete/1".to_string(),
            label: "Delete",
            variant: ActionVariant::Danger,
            hidden: &[],
            confirm: None,
        };

        let html = button.render().into_string();
        assert!(html.contains(r#"<form method="post" class="action-form" action="/delete/1">"#));
        assert!(
            html.contains(r#"<button type="submit" class="action-button danger">Delete</button>"#)
        );
    }

    #[test]
    fn action_button_renders_hidden_inputs() {
        let hidden = [("actor_url", "https://example.com/user")];
        let button = ActionButton {
            action: "/unfollow".to_string(),
            label: "Unfollow",
            variant: ActionVariant::Quiet,
            hidden: &hidden,
            confirm: None,
        };

        let html = button.render().into_string();
        assert!(html.contains(
            r#"<input type="hidden" name="actor_url" value="https://example.com/user">"#
        ));
    }

    #[test]
    fn action_button_escapes_hidden_values() {
        let hidden = [("value", r#"test"with"quotes"#)];
        let button = ActionButton {
            action: "/test".to_string(),
            label: "Test",
            variant: ActionVariant::Quiet,
            hidden: &hidden,
            confirm: None,
        };

        let html = button.render().into_string();
        assert!(html.contains(r#"value="test&quot;with&quot;quotes""#));
    }

    #[test]
    fn action_button_renders_confirm_dialog() {
        let button = ActionButton {
            action: "/delete".to_string(),
            label: "Delete",
            variant: ActionVariant::Danger,
            hidden: &[],
            confirm: Some("Delete this item?"),
        };

        let html = button.render().into_string();
        assert!(html.contains("<details class=\"confirm\">"));
        assert!(html.contains("<summary>Delete</summary>"));
        assert!(html.contains("Delete this item?"));
    }

    #[test]
    fn action_button_variants() {
        let quiet = ActionButton {
            action: "/test".to_string(),
            label: "Quiet",
            variant: ActionVariant::Quiet,
            hidden: &[],
            confirm: None,
        };
        assert!(quiet.render().into_string().contains("action-button quiet"));

        let active = ActionButton {
            action: "/test".to_string(),
            label: "Active",
            variant: ActionVariant::Active,
            hidden: &[],
            confirm: None,
        };
        assert!(
            active
                .render()
                .into_string()
                .contains("action-button active")
        );

        let solid = ActionButton {
            action: "/test".to_string(),
            label: "Solid",
            variant: ActionVariant::Solid,
            hidden: &[],
            confirm: None,
        };
        assert!(solid.render().into_string().contains("action-button solid"));
    }
}
