use actix_web::HttpResponse;
use maud::{Markup, PreEscaped, Render, html};

use super::{
    Page,
    components::{
        AuthorName, Complaint, DeleteForm, FilterLink, RepositoryChrome, RepositoryTab, TextArea,
        TextField,
    },
};

/// A summary as a ticket's heading shows it: markdown, when what the
/// markdown made of it is a single paragraph that fits in a heading, and
/// `None` when it is not - which is the caller's cue to write the summary
/// out as the plain text it also is.
///
/// The HTML that comes back is [`crate::markdown`]'s own sanitized output,
/// so it is safe to embed the way a readme is; a summary of several
/// paragraphs, or of raw HTML that was all stripped away, embeds as escaped
/// text instead of dropping half a document into a heading.
pub(super) fn inline_markdown(summary: &str) -> Option<String> {
    let rendered = crate::markdown::render(summary);
    let rendered = rendered.trim();
    let paragraph = rendered.strip_prefix("<p>")?.strip_suffix("</p>")?;

    // Two paragraphs are a `</p>` of their own in the middle of this one,
    // and are no fit for a heading at all.
    (!paragraph.contains("</p>")).then(|| paragraph.to_string())
}

/// A repository's tickets, as the list page shows them.
pub struct TicketsDetail {
    pub slug: String,
    pub visibility: &'static str,
    /// The branch the repository opens on, for the browse tab.
    pub default_branch: String,
    /// The labels this repository has, each one a way to filter the list by
    /// it.
    pub labels: Vec<String>,
    /// The labels the list is showing, which is what the search carries on
    /// to the next request.
    pub selected: Vec<String>,
    /// The state filter currently applied ("open" or "closed").
    pub state_filter: String,
    /// The words the list is searched by, empty when it is not being
    /// searched.
    pub query: String,
    /// The tickets the filter and the search left, newest first.
    pub tickets: Vec<TicketRow>,
    /// Whether the visitor is signed in, and so has somebody to open a
    /// ticket as. A stranger is not offered a way they cannot use.
    pub can_open: bool,
    /// Whether the visitor owns the repository, and so may make the labels
    /// it is filed under.
    pub can_label: bool,
    /// The label form's name and complaint, when the last attempt at one was
    /// refused.
    pub label_name: Option<String>,
    pub label_error: Option<String>,
}

/// One ticket as the list shows it.
pub struct TicketRow {
    pub number: i64,
    /// The summary as it was written: plain text here, because a list is
    /// scanned rather than read, and a ticket's own page is where its
    /// markdown belongs.
    pub summary: String,
    /// Who opened it, by name.
    pub creator: crate::comments::CommentAuthor,
    /// The labels it carries, in the order the repository made them.
    pub labels: Vec<String>,
}

pub(super) struct TicketsBody<'a> {
    owner: &'a str,
    detail: &'a TicketsDetail,
    viewer: Option<&'a str>,
}

impl Render for TicketsBody<'_> {
    fn render(&self) -> Markup {
        let detail = self.detail;
        let base = format!("/~{}/{}", self.owner, detail.slug);

        html! {
            (RepositoryChrome {
                owner: self.owner,
                slug: &detail.slug,
                visibility: detail.visibility,
                default_branch: &detail.default_branch,
                current_tab: RepositoryTab::Tickets,
                is_owner: self.viewer == Some(self.owner),
            })

            // State tabs like patches have: Open and Closed
            nav class="ticket-filters" {
                (FilterLink {
                    href: &format!("?state=open&labels={}", detail.selected.join(",")),
                    label: "Open",
                    base_class: "ticket-label",
                    selected: detail.state_filter == "open",
                })
                (FilterLink {
                    href: &format!("?state=closed&labels={}", detail.selected.join(",")),
                    label: "Closed",
                    base_class: "ticket-label",
                    selected: detail.state_filter == "closed",
                })
            }

            // Label filters
            nav class="ticket-label-filters" {
                (FilterLink {
                    href: &format!("?state={}", detail.state_filter),
                    label: "All labels",
                    base_class: "ticket-label",
                    selected: detail.selected.is_empty(),
                })
                @for label in &detail.labels {
                    (FilterLink {
                        href: &format!("?state={}&labels={}", detail.state_filter, crate::tickets::encode(label)),
                        label,
                        base_class: "ticket-label",
                        selected: detail.selected.contains(label),
                    })
                }
            }

            form class="ticket-search" method="get" action=(format!("{base}/tickets")) {
                // The state and label filters are carried through the search
                input type="hidden" name="state" value=(&detail.state_filter);
                input type="hidden" name="labels" value=(detail.selected.join(","));
                label {
                    "Search"
                    input type="search" name="q" value=(&detail.query);
                }
                button type="submit" { "Search" }
            }

            @if detail.can_open {
                p class="ticket-tools" {
                    a href=(format!("{base}/tickets/new")) { "New ticket" }
                }
            }

            @if detail.tickets.is_empty() {
                p class="empty" { "Nothing here yet." }
            } @else {
                ul class="tickets" {
                    @for ticket in &detail.tickets {
                        li {
                            a class="ticket-summary"
                                href=(format!("{base}/tickets/{}", ticket.number)) {
                                "#" (ticket.number) " " (ticket.summary)
                            }
                            span class="ticket-creator" {
                                "opened by "
                                (AuthorName(&ticket.creator))
                            }
                            @for label in &ticket.labels {
                                // A label on a ticket is a filter on the
                                // list, replacing whatever it is showing
                                // with everything filed under it.
                                a class="ticket-label"
                                    href=(format!("?labels={}", crate::tickets::encode(label))) {
                                    (label)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

/// What the ticket form shows when it is (re)rendered.
pub struct TicketForm {
    /// Where the form posts: the collection when a ticket is being opened,
    /// the ticket's own address when it is being edited.
    pub action: String,
    pub heading: String,
    /// The repository the form is for, for the tab bar and the heading that
    /// say where the visitor is.
    pub slug: String,
    pub visibility: &'static str,
    pub default_branch: String,
    pub summary: Option<String>,
    pub description: Option<String>,
    /// The state of the ticket: "open" or "closed".
    pub state: String,
    /// The labels the form comes back with checked, which is what saving
    /// without touching them keeps.
    pub selected: Vec<String>,
    /// The labels the repository has, each one offered as a checkbox.
    pub labels: Vec<String>,
    pub error: Option<String>,
    /// What the button says: opening a ticket and saving one are not the
    /// same act even though they are the same form.
    pub submit: &'static str,
}

pub(super) struct TicketFormBody<'a> {
    owner: &'a str,
    slug: &'a str,
    visibility: &'a str,
    default_branch: &'a str,
    form: &'a TicketForm,
    viewer: Option<&'a str>,
}

impl Render for TicketFormBody<'_> {
    fn render(&self) -> Markup {
        html! {
            (RepositoryChrome {
                owner: self.owner,
                slug: self.slug,
                visibility: self.visibility,
                default_branch: self.default_branch,
                current_tab: RepositoryTab::Tickets,
                is_owner: self.viewer == Some(self.owner),
            })
            h3 { (self.form.heading) }
            (Complaint(self.form.error.as_deref()))
            form method="post" action=(&self.form.action) class="ticket-form" {
                (TextField {
                    label: Some("Summary"),
                    name: "summary",
                    input_type: "text",
                    value: self.form.summary.as_deref(),
                    required: true,
                    maxlength: u32::MAX,
                })
                (TextArea {
                    label: Some("Description"),
                    name: "description",
                    rows: 8,
                    value: self.form.description.as_deref(),
                    required: false,
                })
                fieldset {
                    legend { "State" }
                    label {
                        input type="radio" name="state" value="open" checked[self.form.state == "open"];
                        "Open"
                    }
                    label {
                        input type="radio" name="state" value="closed" checked[self.form.state == "closed"];
                        "Closed"
                    }
                }
                fieldset {
                    legend { "Labels" }
                    @for label in &self.form.labels {
                        label {
                            input type="checkbox" name="labels" value=(label)
                                checked[self.form.selected.contains(label)];
                            (label)
                        }
                    }
                }
                button type="submit" { (self.form.submit) }
            }
        }
    }
}

/// One ticket, as its own page shows it.
pub struct TicketDetail<'a> {
    pub slug: String,
    pub visibility: &'static str,
    /// The branch the repository opens on, for the browse tab.
    pub default_branch: String,
    pub number: i64,
    /// The summary as it was written; [`inline_markdown`] decides on the way
    /// out whether the heading shows it as markdown or as the text it is.
    pub summary: String,
    /// The description, rendered from markdown and sanitized - this page
    /// says what a ticket is about in the words whoever opened it wrote.
    pub description_html: String,
    /// Who opened it, by name.
    pub creator: crate::comments::CommentAuthor,
    /// When it was opened, already formatted.
    pub opened_at: String,
    /// The state of the ticket: "open" or "closed".
    pub state: String,
    /// The labels it carries, in the order the repository made them.
    pub labels: Vec<String>,
    /// Whether the visitor may edit or delete the ticket.
    pub may_change: bool,
    /// The attachments on this ticket, with their metadata.
    pub attachments: Vec<AttachmentView>,
    /// The comments on this ticket, in chronological order.
    pub comments: Vec<super::comments::CommentView<'a>>,
    /// Whether the visitor may comment on this ticket.
    pub may_comment: bool,
    /// The complaint, if the last comment attempt was refused.
    pub comment_error: Option<String>,
    /// The draft, if the last comment attempt was refused.
    pub comment_draft: Option<String>,
}

/// An attachment on a ticket, as the detail page shows it.
pub struct AttachmentView {
    /// The UUID used in the download URL.
    pub uuid: String,
    /// The original filename.
    pub filename: String,
    /// The size in bytes.
    pub size: i64,
    /// The MIME type.
    pub content_type: String,
}

pub(super) struct TicketDetailBody<'a> {
    owner: &'a str,
    detail: &'a TicketDetail<'a>,
    viewer: Option<&'a str>,
}

impl Render for TicketDetailBody<'_> {
    fn render(&self) -> Markup {
        let detail = self.detail;
        let base = format!("/~{}/{}", self.owner, detail.slug);

        html! {
            (RepositoryChrome {
                owner: self.owner,
                slug: &detail.slug,
                visibility: detail.visibility,
                default_branch: &detail.default_branch,
                current_tab: RepositoryTab::Tickets,
                is_owner: self.viewer == Some(self.owner),
            })
            div class="ticket-header" {
                h3 {
                    "#" (detail.number) " "
                    @match inline_markdown(&detail.summary) {
                        Some(summary) => (PreEscaped(summary)),
                        None => (detail.summary.as_str()),
                    }
                }
                @if detail.may_change {
                    div class="ticket-tools" {
                        a href=(format!("{base}/tickets/{}/edit", detail.number)) class="ticket-edit-link" { "Edit" }
                        (DeleteForm {
                            action: format!("{base}/tickets/{}/delete", detail.number),
                            form_class: Some("ticket-delete"),
                            button_class: Some("danger"),
                            confirm: None,
                        })
                    }
                }
            }
            p class="ticket-meta" {
                "opened by "
                (AuthorName(&detail.creator))
                " on " (detail.opened_at)
            }
            p class="ticket-state" {
                "State: " (detail.state)
            }
            @if !detail.labels.is_empty() {
                p class="ticket-labels" {
                    @for label in &detail.labels {
                        span class="ticket-label" { (label) }
                    }
                }
            }
            // Rendered and sanitized by `crate::markdown`, the same as a
            // readme: this is the other place a page embeds markup it did
            // not write itself.
            section class="ticket-description" {
                (PreEscaped(&detail.description_html))
            }
            @if !detail.attachments.is_empty() {
                section class="ticket-attachments" {
                    h4 { "Attachments" }
                    ul {
                        @for attachment in &detail.attachments {
                            li {
                                a href=(format!("{base}/tickets/{}/attachments/{}", detail.number, attachment.uuid)) {
                                    (attachment.filename)
                                }
                                " (" (attachment.size) " bytes, " (attachment.content_type) ")"
                            }
                        }
                    }
                }
            }
            (super::comments::CommentListView {
                comments: &detail.comments,
                may_comment: detail.may_comment,
                base_comments_url: &format!("{base}/tickets/{}/comments", detail.number),
                comment_draft: detail.comment_draft.as_deref(),
                comment_error: detail.comment_error.as_deref()
            })
        }
    }
}

/// The page listing a repository's tickets, filtered and searched as it is
/// asked to be.
pub fn tickets(viewer: Option<&str>, owner: &str, detail: &TicketsDetail) -> HttpResponse {
    Page {
        title: format!("~{owner}/{}: Tickets - Frogue", detail.slug),
        viewer: viewer.map(str::to_string),
        extra_head: None,
        body: TicketsBody {
            owner,
            detail,
            viewer,
        },
    }
    .into_response()
}

/// The form for opening a ticket on `owner`'s repository, or for editing the
/// one at `form.action`.
pub fn ticket_form(viewer: Option<&str>, owner: &str, form: &TicketForm) -> HttpResponse {
    Page {
        title: format!("~{owner}: {} - Frogue", form.heading),
        viewer: viewer.map(str::to_string),
        extra_head: None,
        body: TicketFormBody {
            owner,
            slug: &form.slug,
            visibility: form.visibility,
            default_branch: &form.default_branch,
            form,
            viewer,
        },
    }
    .into_response()
}

/// The page showing one ticket on `owner`'s repository.
pub fn ticket(viewer: Option<&str>, owner: &str, detail: &TicketDetail) -> HttpResponse {
    Page {
        title: format!("~{owner}/{}: #{} - Frogue", detail.slug, detail.number),
        viewer: viewer.map(str::to_string),
        extra_head: None,
        body: TicketDetailBody {
            owner,
            detail,
            viewer,
        },
    }
    .into_response()
}

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

    #[test]
    fn a_summary_that_is_one_paragraph_comes_back_as_the_html_of_it() {
        assert_eq!(
            inline_markdown("Cranberry sauce"),
            Some("Cranberry sauce".to_string())
        );
        assert_eq!(
            inline_markdown("A **bold** sauce"),
            Some("A <strong>bold</strong> sauce".to_string())
        );
        // What comes back is rendered and sanitized, so a summary that
        // tried to be markup is still not markup.
        assert_eq!(inline_markdown("<script>alert(1)</script>"), None);
    }

    #[test]
    fn a_summary_that_is_not_one_paragraph_leaves_it_to_the_page_to_escape() {
        // Two paragraphs are no fit for a heading; the caller writes the
        // summary out as text instead, escaped the way maud escapes
        // everything it interpolates.
        assert_eq!(inline_markdown("one\n\ntwo"), None);
        // Raw HTML that was all stripped away leaves nothing to put in a
        // heading either.
        assert_eq!(inline_markdown("<iframe src=x>"), None);
        // An empty summary is no heading at all, and says nothing twice.
        assert_eq!(inline_markdown(""), None);
    }
}
