/* repo_row.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::path::PathBuf;

use adw::prelude::*;
use gettextrs::{gettext, ngettext};
use relm4::factory::FactoryComponent;
use relm4::gtk::{gdk, glib};
use relm4::prelude::*;

use crate::git::QuickStatus;
use crate::repository::Repository;
use crate::ui_helpers;

pub struct RepoRow {
    repository: Repository,
    index: DynamicIndex,
    status: Option<QuickStatus>,
    draggable: bool,
    popover: Option<gtk::PopoverMenu>,
}

impl std::fmt::Debug for RepoRow {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RepoRow")
            .field("repository", &self.repository)
            .field("index", &self.index)
            .finish()
    }
}

impl RepoRow {
    pub fn id(&self) -> &str {
        &self.repository.id
    }

    fn is_invalid(&self) -> bool {
        self.status.as_ref().is_some_and(|s| !s.valid)
    }

    fn prefix_icon_name(&self) -> &'static str {
        if self.is_invalid() {
            "dialog-warning-symbolic"
        } else if self.draggable {
            "draggable-symbolic"
        } else {
            "folder-symbolic"
        }
    }

    fn prefix_icon_css(&self) -> &'static [&'static str] {
        if self.is_invalid() {
            &["error"]
        } else if self.draggable {
            &["dim-label"]
        } else {
            &[]
        }
    }

    fn prefix_icon_tooltip(&self) -> Option<String> {
        if self.is_invalid() {
            Some(gettext("Repository not found"))
        } else if self.draggable {
            Some(gettext("Drag to reorder"))
        } else {
            None
        }
    }
}

#[derive(Debug)]
pub enum RepoRowInput {
    ShowContextMenu,
    Activate,
    RenameRequested,
    FavoriteClicked,
    SetStatus(QuickStatus),
}

#[derive(Debug)]
pub enum RepoRowOutput {
    Activated {
        path: PathBuf,
        id: String,
        name: String,
    },
    Remove(String),
    Rename(String, String),
    ToggleFavorite(String),
    Reorder {
        dragged_id: String,
        target_id: String,
        before: bool,
    },
}

#[derive(Debug, Clone)]
pub struct RepoRowInit {
    pub repository: Repository,
    pub draggable: bool,
}

fn context_menu(favorite: bool) -> gtk::gio::Menu {
    let menu = gtk::gio::Menu::new();

    let label = if favorite {
        gettext("Unpin from Favorites")
    } else {
        gettext("Pin to Favorites")
    };

    menu.append(Some(&label), Some("repo.toggle-favorite"));
    menu.append(Some(&gettext("Open in File Manager")), Some("repo.open-in-file-manager"));
    menu.append(Some(&gettext("Copy Path")), Some("repo.copy-path"));
    menu.append(Some(&gettext("Rename")), Some("repo.rename"));
    menu.append(Some(&gettext("Remove")), Some("repo.remove"));

    menu
}

fn build_subtitle(repo: &Repository, status: Option<&QuickStatus>) -> String {
    let mut parts: Vec<String> = Vec::new();

    match status {
        Some(s) if !s.valid => return gettext("Repository not found"),

        Some(s) => {
            if let Some(branch) = &s.branch {
                parts.push(branch.clone());
            }

            if s.dirty {
                parts.push(gettext("modified"));
            }
        }

        None => {}
    }

    if let Some(ts) = repo.last_opened {
        parts.push(format_relative_time(ts));
    }

    if parts.is_empty() {
        repo.path.to_string_lossy().into_owned()
    } else {
        parts.join(" · ")
    }
}

fn format_relative_time(ts: i64) -> String {
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs() as i64)
        .unwrap_or(0);

    let diff = now.saturating_sub(ts).max(0);

    if diff < 60 {
        gettext("opened just now")
    } else if diff < 3600 {
        let m = diff / 60;
        // Translators: {} is replaced by a number of minutes
        ngettext("opened {} min ago", "opened {} mins ago", m as u32).replace("{}", &m.to_string())
    } else if diff < 86_400 {
        let h = diff / 3600;
        // Translators: {} is replaced by a number of hours
        ngettext("opened {} hour ago", "opened {} hours ago", h as u32).replace("{}", &h.to_string())
    } else if diff < 86_400 * 30 {
        let d = diff / 86_400;
        // Translators: {} is replaced by a number of days
        ngettext("opened {} day ago", "opened {} days ago", d as u32).replace("{}", &d.to_string())
    } else {
        let dt = glib::DateTime::from_unix_local(ts);
        match dt.and_then(|d| d.format("%Y-%m-%d")) {
            // Translators: {} is replaced by a date in the format YYYY-MM-DD
            Ok(s) => gettext("opened {}").replace("{}", &s),
            Err(_) => gettext("opened earlier"),
        }
    }
}

#[relm4::factory(pub)]
impl FactoryComponent for RepoRow {
    type Init = RepoRowInit;
    type Input = RepoRowInput;
    type Output = RepoRowOutput;
    type CommandOutput = ();
    type ParentWidget = gtk::ListBox;

    view! {
        #[name = "row"]
        adw::ActionRow {
            set_use_markup: false,
            #[watch]
            set_class_active: ("dim-label", self.is_invalid()),
            #[watch]
            set_title: &self.repository.name,
            #[watch]
            set_subtitle: &build_subtitle(&self.repository, self.status.as_ref()),
            set_subtitle_lines: 1,
            #[watch]
            set_activatable: !self.is_invalid(),
            #[watch]
            set_tooltip_text: Some(&self.repository.path.to_string_lossy()),

            add_prefix = &gtk::Image {
                #[watch]
                set_icon_name: Some(self.prefix_icon_name()),
                #[watch]
                set_css_classes: self.prefix_icon_css(),
                #[watch]
                set_tooltip_text: self.prefix_icon_tooltip().as_deref(),
                #[watch]
                update_property: &[gtk::accessible::Property::Label(&self.prefix_icon_tooltip().unwrap_or_default())],
            },

            #[name = "favorite_button"]
            add_suffix = &gtk::Button {
                #[watch]
                set_icon_name: if self.repository.favorite { "starred-symbolic" } else { "non-starred-symbolic" },
                set_valign: gtk::Align::Center,
                add_css_class: "flat",
                #[watch]
                set_tooltip_text: Some(&if self.repository.favorite {
                    gettext("Unpin from Favorites")
                } else {
                    gettext("Pin to Favorites")
                }),
                connect_clicked[sender] => move |_| {
                    sender.input(RepoRowInput::FavoriteClicked);
                },
            },

            add_suffix = &gtk::Image {
                set_icon_name: Some("go-next-symbolic"),
                add_css_class: "dim-label",
                set_accessible_role: gtk::AccessibleRole::Presentation,
                #[watch]
                set_visible: !self.is_invalid(),
            },

            add_controller = gtk::GestureClick {
                set_button: gdk::BUTTON_SECONDARY,
                connect_released[sender] => move |gesture, _, _, _| {
                    gesture.set_state(gtk::EventSequenceState::Claimed);
                    sender.input(RepoRowInput::ShowContextMenu);
                },
            },

            connect_activated[sender] => move |_| {
                sender.input(RepoRowInput::Activate);
            },
        }
    }

    fn init_model(init: Self::Init, index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
        Self {
            repository: init.repository,
            index: index.clone(),
            status: None,
            draggable: init.draggable,
            popover: None,
        }
    }

    fn init_widgets(
        &mut self,
        _index: &DynamicIndex,
        root: Self::Root,
        _returned_widget: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
        sender: FactorySender<Self>,
    ) -> Self::Widgets {
        let widgets = view_output!();

        let popover = gtk::PopoverMenu::from_model(Some(&context_menu(self.repository.favorite)));
        popover.set_parent(&widgets.row);
        popover.set_has_arrow(false);
        self.popover = Some(popover);

        let group = gtk::gio::SimpleActionGroup::new();

        let open_fm_action = gtk::gio::SimpleAction::new("open-in-file-manager", None);
        let path = self.repository.path.clone();
        open_fm_action.connect_activate(move |_, _| {
            ui_helpers::open_in_file_manager(&path);
        });
        group.add_action(&open_fm_action);

        let copy_path_action = gtk::gio::SimpleAction::new("copy-path", None);
        let path = self.repository.path.clone();
        copy_path_action.connect_activate(move |_, _| {
            if let Some(display) = gdk::Display::default() {
                display.clipboard().set_text(&path.to_string_lossy());
            }
        });
        group.add_action(&copy_path_action);

        let rename_action = gtk::gio::SimpleAction::new("rename", None);
        let s = sender.clone();
        rename_action.connect_activate(move |_, _| {
            s.input(RepoRowInput::RenameRequested);
        });
        group.add_action(&rename_action);

        let remove_action = gtk::gio::SimpleAction::new("remove", None);
        let s = sender.clone();
        let id = self.repository.id.clone();
        remove_action.connect_activate(move |_, _| {
            s.output(RepoRowOutput::Remove(id.clone())).ok();
        });
        group.add_action(&remove_action);

        let toggle_fav_action = gtk::gio::SimpleAction::new("toggle-favorite", None);
        let s = sender.clone();
        let id = self.repository.id.clone();
        toggle_fav_action.connect_activate(move |_, _| {
            s.output(RepoRowOutput::ToggleFavorite(id.clone())).ok();
        });
        group.add_action(&toggle_fav_action);

        widgets.row.insert_action_group("repo", Some(&group));

        let key_controller = gtk::EventControllerKey::new();
        let s = sender.clone();
        key_controller.connect_key_pressed(move |_, keyval, _, _| match keyval {
            gdk::Key::F2 => {
                s.input(RepoRowInput::RenameRequested);
                glib::Propagation::Stop
            }

            gdk::Key::Delete | gdk::Key::KP_Delete => {
                s.input(RepoRowInput::ShowContextMenu);
                glib::Propagation::Stop
            }

            gdk::Key::Return | gdk::Key::KP_Enter => {
                s.input(RepoRowInput::Activate);
                glib::Propagation::Stop
            }

            _ => glib::Propagation::Proceed,
        });
        widgets.row.add_controller(key_controller);

        if self.draggable {
            attach_drag_source(&widgets.row, &self.repository.id);
            attach_drop_target(&widgets.row, &self.repository.id, &sender);
        }

        widgets
    }

    fn shutdown(&mut self, _widgets: &mut Self::Widgets, _output: relm4::Sender<Self::Output>) {
        if let Some(popover) = self.popover.take() {
            popover.unparent();
        }
    }

    fn update_with_view(&mut self, widgets: &mut Self::Widgets, msg: Self::Input, sender: FactorySender<Self>) {
        match msg {
            RepoRowInput::ShowContextMenu => {
                if let Some(popover) = &self.popover {
                    popover.popup();
                }
            }

            RepoRowInput::Activate => {
                if !self.is_invalid() {
                    sender
                        .output(RepoRowOutput::Activated {
                            path: self.repository.path.clone(),
                            id: self.repository.id.clone(),
                            name: self.repository.name.clone(),
                        })
                        .ok();
                }
            }

            RepoRowInput::RenameRequested => {
                show_rename_dialog(&widgets.row, &self.repository.name, &self.repository.id, &sender);
            }

            RepoRowInput::FavoriteClicked => {
                sender
                    .output(RepoRowOutput::ToggleFavorite(self.repository.id.clone()))
                    .ok();
            }

            RepoRowInput::SetStatus(status) => {
                self.status = Some(status);
            }
        }

        self.update_view(widgets, sender);
    }
}

fn attach_drag_source(row: &adw::ActionRow, id: &str) {
    let drag_source = gtk::DragSource::new();
    drag_source.set_actions(gdk::DragAction::MOVE);

    let id_for_prepare = id.to_string();
    drag_source.connect_prepare(move |_src, _x, _y| Some(gdk::ContentProvider::for_value(&id_for_prepare.to_value())));

    let row_clone = row.clone();
    drag_source.connect_drag_begin(move |src, _drag| {
        let paintable = gtk::WidgetPaintable::new(Some(&row_clone));
        src.set_icon(Some(&paintable), 0, 0);
    });

    row.add_controller(drag_source);
}

fn attach_drop_target(row: &adw::ActionRow, id: &str, sender: &FactorySender<RepoRow>) {
    let drop_target = gtk::DropTarget::new(glib::Type::STRING, gdk::DragAction::MOVE);

    let target_id = id.to_string();
    let sender = sender.clone();
    let row_weak = row.downgrade();
    drop_target.connect_drop(move |_target, value, _x, y| {
        let Ok(dragged_id) = value.get::<String>() else {
            return false;
        };

        if dragged_id == target_id {
            return false;
        }

        let height = row_weak.upgrade().map(|r| r.height()).unwrap_or(0) as f64;
        let before = height > 0.0 && y < height / 2.0;

        sender
            .output(RepoRowOutput::Reorder {
                dragged_id,
                target_id: target_id.clone(),
                before,
            })
            .ok();
        true
    });

    row.add_controller(drop_target);
}

fn show_rename_dialog(row: &adw::ActionRow, current_name: &str, id: &str, sender: &FactorySender<RepoRow>) {
    let window = row.ancestor(gtk::Window::static_type()).and_downcast::<gtk::Window>();

    let dialog = adw::AlertDialog::builder()
        .heading(gettext("Rename Repository"))
        .build();

    let entry = gtk::Entry::builder().text(current_name).activates_default(true).build();
    entry.update_property(&[gtk::accessible::Property::Label(&gettext("Repository Name"))]);
    dialog.set_extra_child(Some(&entry));

    let entry_for_focus = entry.clone();
    dialog.connect_realize(move |_| {
        entry_for_focus.grab_focus();
    });

    dialog.add_response("cancel", &gettext("Cancel"));
    dialog.add_response("rename", &gettext("Rename"));
    dialog.set_response_appearance("rename", adw::ResponseAppearance::Suggested);
    dialog.set_default_response(Some("rename"));
    dialog.set_close_response("cancel");
    dialog.set_response_enabled("rename", !entry.text().is_empty());

    let dialog_ref = dialog.clone();
    entry.connect_changed(move |e| {
        dialog_ref.set_response_enabled("rename", !e.text().is_empty());
    });

    let sender = sender.clone();
    let id = id.to_string();

    dialog.connect_response(None, move |_dialog, response| {
        if response == "rename" {
            let new_name = entry.text().to_string();

            if !new_name.is_empty() {
                sender.output(RepoRowOutput::Rename(id.clone(), new_name)).ok();
            }
        }
    });

    dialog.present(window.as_ref());
}
