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

use super::{
    Page,
    commit::{diff_stats_summary, file_diffs},
    components::{
        AuthorName, Complaint, FilterLink, RepositoryChrome, RepositoryTab, TextArea, TextField,
    },
    repositories::CommitSummary,
};

/// A repository's patches, as the list page shows them.
pub struct PatchesDetail {
    pub slug: String,
    pub visibility: &'static str,
    /// The branch the repository opens on, for the browse tab.
    pub default_branch: String,
    /// The patches the filter left, newest first.
    pub patches: Vec<PatchRow>,
    /// Whether the visitor is signed in, and so has somebody to open a
    /// patch as. A stranger is not offered a way they cannot use.
    pub can_open: bool,
    /// What filter is currently applied (draft, open, closed, merged, or all)
    pub state_filter: String,
}

/// One patch as the list shows it.
pub struct PatchRow {
    pub number: i64,
    /// The summary as it was written: plain text here, because a list is
    /// scanned rather than read, and a patch's own page is where its
    /// markdown belongs.
    pub summary: String,
    /// Who opened it, by name.
    pub creator: crate::comments::CommentAuthor,
    /// The status of the patch (draft, open, closed, merged).
    pub status: String,
}

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

impl Render for PatchesBody<'_> {
    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::Patches,
                is_owner: self.viewer == Some(self.owner),
            })

            // State filter tabs
            nav class="patch-filters" {
                (FilterLink {
                    href: "",
                    label: "All",
                    base_class: "patch-label",
                    selected: detail.state_filter == "all",
                })
                (FilterLink {
                    href: "?state=draft",
                    label: "Draft",
                    base_class: "patch-label",
                    selected: detail.state_filter == "draft",
                })
                (FilterLink {
                    href: "?state=open",
                    label: "Open",
                    base_class: "patch-label",
                    selected: detail.state_filter == "open",
                })
                (FilterLink {
                    href: "?state=closed",
                    label: "Closed",
                    base_class: "patch-label",
                    selected: detail.state_filter == "closed",
                })
                (FilterLink {
                    href: "?state=merged",
                    label: "Merged",
                    base_class: "patch-label",
                    selected: detail.state_filter == "merged",
                })
            }

            @if detail.can_open {
                p class="patch-tools" {
                    "Patches are created by pushing to " code { "refs/patches" } " over SSH."
                }
            }

            @if detail.patches.is_empty() {
                p class="empty" { "Nothing here yet." }
            } @else {
                ul class="patches" {
                    @for patch in &detail.patches {
                        li {
                            a class="patch-summary"
                                href=(format!("{base}/patches/{}", patch.number)) {
                                "#" (patch.number) " " (patch.summary)
                            }
                            span class="patch-status" { "(" (patch.status) ")" }
                            span class="patch-creator" {
                                " by "
                                (AuthorName(&patch.creator))
                            }
                        }
                    }
                }
            }
        }
    }
}

/// A patch's detail page, as the viewer's own page shows it.
pub struct PatchDetail<'a> {
    pub slug: String,
    pub visibility: &'static str,
    pub default_branch: String,
    pub number: i64,
    pub summary: String,
    pub description_html: String,
    pub creator: crate::comments::CommentAuthor,
    pub created_at: String,
    pub status: String,
    pub source_ref: String,
    pub target_branch: String,
    pub merge_policy: String,
    /// Whether the viewer may change this patch.
    pub may_change: bool,
    /// Whether the viewer is the repository owner.
    pub is_owner: bool,
    /// The commits in this patch, newest first.
    pub commits: Vec<CommitSummary>,
    /// The files changed in this patch.
    pub files: Vec<crate::repositories::git::DiffFile>,
    /// If the diff cannot be shown, the reason (e.g. merged before snapshots existed).
    pub diff_unavailable_reason: Option<String>,
    /// The comments on this patch, in chronological order.
    pub comments: Vec<super::comments::CommentView<'a>>,
    /// Whether the visitor may comment on this patch.
    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>,
    /// The jj change-id, if this patch carries one.
    pub change_id: Option<String>,
}

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

impl Render for PatchBody<'_> {
    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::Patches,
                is_owner: self.viewer == Some(self.owner),
            })

            article class="patch-detail" {
                header {
                    div class="patch-header-top" {
                        h2 { "#" (detail.number) " " (detail.summary) }
                        @if detail.may_change {
                            a href=(format!("{base}/patches/{}/edit", detail.number)) class="patch-edit-link" { "Edit" }
                        }
                    }
                    dl class="patch-meta" {
                        dt { "Status" }
                        dd { (detail.status) }
                        dt { "Author" }
                        dd {
                            (AuthorName(&detail.creator))
                        }
                        dt { "Opened" }
                        dd { (detail.created_at) }
                        dt { "Source" }
                        dd { code { (detail.source_ref) } }
                        dt { "Target" }
                        dd { code { (detail.target_branch) } }
                        dt { "Merge policy" }
                        dd { (detail.merge_policy) }
                        @if let Some(change_id) = &detail.change_id {
                            dt { "Change ID" }
                            dd { code class="change-id" { (change_id) } }
                        }
                    }
                }

                section class="patch-description" {
                    (PreEscaped(detail.description_html.clone()))
                }

                @if !detail.commits.is_empty() {
                    section class="patch-commits" {
                        h3 { "Commits (" (detail.commits.len()) ")" }
                        ul class="commits" {
                            @for commit in &detail.commits {
                                li {
                                    (super::commits::CommitLine {
                                        owner: self.owner,
                                        slug: &detail.slug,
                                        commit,
                                    })
                                }
                            }
                        }
                    }
                }

                @if !detail.files.is_empty() {
                    section class="patch-diff" {
                        h3 { "Changes" }
                        div class="commit-stats" {
                            (diff_stats_summary(&detail.files))
                        }
                        div class="commit-diff" {
                            (file_diffs(&detail.files))
                        }
                    }
                }

                @if let Some(reason) = &detail.diff_unavailable_reason {
                    section class="patch-diff-unavailable" {
                        p { (reason) }
                    }
                }

                @if detail.may_change && detail.is_owner && (detail.status == "draft" || detail.status == "open") {
                    section class="patch-actions" {
                        form method="post" action=(format!("{base}/patches/{}/merge", detail.number)) class="inline-form" {
                            button type="submit" class="merge-button" { "Merge" }
                        }
                    }
                }

                (super::comments::CommentListView {
                    comments: &detail.comments,
                    may_comment: detail.may_comment,
                    base_comments_url: &format!("{base}/patches/{}/comments", detail.number),
                    comment_draft: detail.comment_draft.as_deref(),
                    comment_error: detail.comment_error.as_deref()
                })
            }
        }
    }
}

/// What a patch edit form is asked to show.
pub struct PatchEditForm {
    pub action: String,
    pub slug: String,
    pub visibility: &'static str,
    pub default_branch: String,
    pub number: i64,
    pub summary: Option<String>,
    pub description: Option<String>,
    pub target_branch: String,
    pub branches: Vec<String>,
    pub status: String,
    pub merge_policy: String,
    pub error: Option<String>,
    pub submit: &'static str,
    /// Whether the viewer is the repository owner (can change merge policy).
    pub is_owner: bool,
}

pub(super) struct PatchEditBody<'a> {
    owner: &'a str,
    form: &'a PatchEditForm,
    viewer: Option<&'a str>,
}

impl Render for PatchEditBody<'_> {
    fn render(&self) -> Markup {
        let form = self.form;

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

            @if let Some(error) = &form.error {
                (Complaint(Some(error)))
            }

            form method="post" action=(form.action) class="patch-edit" {
                fieldset {
                    legend { "Summary" }
                    (TextField {
                        name: "summary",
                        label: None,
                        input_type: "text",
                        value: form.summary.as_deref(),
                        required: true,
                        maxlength: 255,
                    })
                }
                fieldset {
                    legend { "Description" }
                    (TextArea {
                        name: "description",
                        label: None,
                        rows: 10,
                        value: form.description.as_deref(),
                        required: false,
                    })
                }
                fieldset {
                    legend { "Target branch" }
                    select name="target_branch" required {
                        @for branch in &form.branches {
                            option value=(branch) selected[branch == &form.target_branch] { (branch) }
                        }
                    }
                }
                fieldset {
                    legend { "Status" }
                    select name="status" {
                        option value="draft" selected[form.status == "draft"] { "Draft" }
                        option value="open" selected[form.status == "open"] { "Open" }
                        option value="closed" selected[form.status == "closed"] { "Closed" }
                    }
                }
                @if form.is_owner {
                    fieldset {
                        legend { "Merge policy" }
                        (super::form::MergePolicySelect {
                            name: "merge_policy",
                            current: &form.merge_policy,
                        })
                    }
                }
                button type="submit" { (form.submit) }
            }
        }
    }
}

/// A repository's patches, as the list page shows them.
pub fn patches(viewer: Option<&str>, owner: &str, detail: &PatchesDetail) -> HttpResponse {
    Page {
        title: format!("{owner}/{}/patches", detail.slug),
        viewer: viewer.map(str::to_string),
        extra_head: None,
        body: PatchesBody {
            owner,
            detail,
            viewer,
        },
    }
    .into_response()
}

/// A patch's own page.
#[instrument(skip(detail), fields(patch_slug=detail.slug))]
pub fn patch(viewer: Option<&str>, owner: &str, detail: &PatchDetail) -> HttpResponse {
    Page {
        title: format!("{owner}/{}/patches/{}", detail.slug, detail.number),
        viewer: viewer.map(str::to_string),
        extra_head: None,
        body: PatchBody {
            owner,
            detail,
            viewer,
        },
    }
    .into_response()
}

/// A patch edit form.
pub fn patch_form(viewer: Option<&str>, owner: &str, form: &PatchEditForm) -> HttpResponse {
    Page {
        title: format!("{owner}/{}/patches/{}/edit", form.slug, form.number),
        viewer: viewer.map(str::to_string),
        extra_head: None,
        body: PatchEditBody {
            owner,
            form,
            viewer,
        },
    }
    .into_response()
}
