/* credential_dialog.rs
 *
 * Copyright 2026 Christian Kruse
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

use std::cell::RefCell;
use std::rc::Rc;

use adw::prelude::*;
use gettextrs::gettext;
use relm4::gtk;
use tokio::sync::oneshot;

#[derive(Debug, Clone)]
pub enum CredentialPrompt {
    Username { url: String },
    Password { url: String, username: Option<String> },
    Other { prompt: String },
}

impl CredentialPrompt {
    pub fn parse(prompt: &str) -> Self {
        let trimmed = prompt.trim().trim_end_matches(':').trim();

        if let Some(rest) = trimmed.strip_prefix("Username for ")
            && let Some(url) = strip_quoted(rest)
        {
            return CredentialPrompt::Username { url };
        }

        if let Some(rest) = trimmed.strip_prefix("Password for ")
            && let Some(target) = strip_quoted(rest)
        {
            let (url, username) = split_user_from_url(&target);
            return CredentialPrompt::Password { url, username };
        }

        CredentialPrompt::Other {
            prompt: prompt.to_string(),
        }
    }
}

fn strip_quoted(s: &str) -> Option<String> {
    let s = s.trim();
    let s = s.strip_prefix('\'').unwrap_or(s);
    let s = s.strip_suffix('\'').unwrap_or(s);

    if s.is_empty() { None } else { Some(s.to_string()) }
}

fn split_user_from_url(target: &str) -> (String, Option<String>) {
    if let Ok(parsed) = url::Url::parse(target) {
        let user = if parsed.username().is_empty() {
            None
        } else {
            Some(parsed.username().to_string())
        };

        let scheme = parsed.scheme();
        let host = parsed.host_str().unwrap_or("");
        let port = parsed.port();

        let url = match port {
            Some(p) => format!("{scheme}://{host}:{p}"),
            None => format!("{scheme}://{host}"),
        };

        (url, user)
    } else {
        (target.to_string(), None)
    }
}

#[derive(Debug)]
pub struct DialogAnswer {
    pub username: Option<String>,
    pub password: Option<String>,
}

pub type Reply = oneshot::Sender<Option<DialogAnswer>>;

pub fn open(prompt: CredentialPrompt, reply: Reply) {
    relm4::view! {
        #[name = "dialog"]
        adw::AlertDialog {
            set_heading: Some(&heading_for(&prompt)),
            set_body: &body_for(&prompt),

            add_response: ("cancel", &gettext("Cancel")),
            add_response: ("confirm", &gettext("Sign In")),
            set_response_appearance: ("confirm", adw::ResponseAppearance::Suggested),
            set_default_response: Some("confirm"),
            set_close_response: "cancel",
        }
    }

    relm4::view! {
        #[name = "content"]
        gtk::Box {
            set_orientation: gtk::Orientation::Vertical,
            add_css_class: "gap-l",
        }
    }

    relm4::view! {
        #[name = "username_entry"]
        gtk::Entry {
            set_activates_default: true,
        }
    }

    relm4::view! {
        #[name = "password_entry"]
        gtk::PasswordEntry {
            set_activates_default: true,
        }
    }

    dialog.set_extra_child(Some(&content));

    let username_label = gettext("Username");
    let password_label = gettext("Password");
    username_entry.update_property(&[gtk::accessible::Property::Label(&username_label)]);
    password_entry.update_property(&[gtk::accessible::Property::Label(&password_label)]);

    let need_username =
        matches!(&prompt, CredentialPrompt::Username { .. } | CredentialPrompt::Password { username: None, .. });

    let need_password = matches!(
        &prompt,
        CredentialPrompt::Username { .. } | CredentialPrompt::Password { .. } | CredentialPrompt::Other { .. }
    );

    if let CredentialPrompt::Password { username: Some(u), .. } = &prompt {
        relm4::view! {
            #[name = "row"]
            gtk::Box {
                set_orientation: gtk::Orientation::Horizontal,
                add_css_class: "gap-m",

                gtk::Label {
                    set_label: &gettext("User:"),
                    set_xalign: 0.0,
                },

                gtk::Label {
                    set_label: u.as_str(),
                    set_xalign: 0.0,
                    set_hexpand: true,
                    set_ellipsize: gtk::pango::EllipsizeMode::End,
                    add_css_class: "monospace",
                },
            }
        }

        content.append(&row);
    }

    if need_username {
        username_entry.set_placeholder_text(Some(&username_label));
        content.append(&username_entry);
    }

    if need_password {
        password_entry.set_placeholder_text(Some(&password_label));
        content.append(&password_entry);
    }

    let parent = active_window();
    let reply = Rc::new(RefCell::new(Some(reply)));

    {
        let reply = reply.clone();
        let prompt = prompt.clone();
        let username_entry = username_entry.clone();
        let password_entry = password_entry.clone();

        dialog.connect_response(None, move |_d, response| {
            tracing::info!("credential_dialog: response={response:?}");

            let Some(reply) = reply.borrow_mut().take() else {
                tracing::warn!("credential_dialog: response fired but reply already taken");
                return;
            };

            if response != "confirm" {
                let _ = reply.send(None);
                return;
            }

            let entered_user = {
                let s = username_entry.text().to_string();
                if s.is_empty() { None } else { Some(s) }
            };
            let entered_pass = {
                let s = password_entry.text().to_string();
                if s.is_empty() { None } else { Some(s) }
            };

            let answer = match &prompt {
                CredentialPrompt::Username { .. } => DialogAnswer {
                    username: entered_user,
                    password: entered_pass,
                },

                CredentialPrompt::Password { username, .. } => DialogAnswer {
                    username: username.clone().or(entered_user),
                    password: entered_pass,
                },

                CredentialPrompt::Other { .. } => DialogAnswer {
                    username: None,
                    password: entered_pass,
                },
            };

            tracing::info!(
                "credential_dialog: sending answer (user: {}, pass: {})",
                answer.username.is_some(),
                answer.password.is_some()
            );
            let _ = reply.send(Some(answer));
        });
    }

    tracing::info!("credential_dialog: presenting dialog (parent: {})", parent.is_some());
    dialog.present(parent.as_ref());
}

fn heading_for(prompt: &CredentialPrompt) -> String {
    match prompt {
        CredentialPrompt::Username { .. } | CredentialPrompt::Password { .. } => gettext("Sign In to Git Server"),
        CredentialPrompt::Other { .. } => gettext("Authentication Required"),
    }
}

fn body_for(prompt: &CredentialPrompt) -> String {
    match prompt {
        CredentialPrompt::Username { url } | CredentialPrompt::Password { url, .. } => {
            gettext("Enter credentials for {url}").replace("{url}", url)
        }

        CredentialPrompt::Other { prompt } => prompt.clone(),
    }
}

fn active_window() -> Option<gtk::Window> {
    let app = relm4::main_application();

    app.active_window()
        .and_then(|w| w.downcast::<gtk::Window>().ok())
        .or_else(|| {
            app.windows()
                .into_iter()
                .next()
                .and_then(|w| w.downcast::<gtk::Window>().ok())
        })
}
