/* askpass/server.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::collections::HashMap;
use std::fmt::Write;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::{UnixListener as StdUnixListener, UnixStream};
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;

use relm4::gtk::glib;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixListener;
use tokio::sync::oneshot;

use super::ipc::{self, AskpassReply, AskpassRequest};
use crate::credential_dialog::{self, CredentialPrompt};
use crate::credentials::CredentialStore;

static SERVER: OnceLock<Server> = OnceLock::new();

#[derive(Clone)]
pub struct Server {
    inner: Arc<Inner>,
}

struct Inner {
    socket_path: PathBuf,
    spawns: Mutex<HashMap<String, SpawnContext>>,
}

#[derive(Default, Clone)]
struct SpawnContext {
    last_username: Option<String>,
}

pub struct Guard {
    socket_path: PathBuf,
}

impl Drop for Guard {
    fn drop(&mut self) {
        let _ = std::fs::remove_file(&self.socket_path);
    }
}

pub fn start() -> Option<Guard> {
    let dir = ipc::socket_dir();
    if let Err(e) = std::fs::create_dir_all(&dir) {
        tracing::warn!("askpass: failed to create socket dir {dir:?}: {e}");
        return None;
    }

    set_dir_mode_0700(&dir);
    cleanup_stale_sockets(&dir);

    let socket_path = ipc::socket_path_for_pid(std::process::id());
    let _ = std::fs::remove_file(&socket_path);

    let std_listener = match bind_std_listener(&socket_path) {
        Ok(l) => l,

        Err(e) => {
            tracing::warn!("askpass: failed to bind {socket_path:?}: {e}");
            return None;
        }
    };

    let server = Server {
        inner: Arc::new(Inner {
            socket_path: socket_path.clone(),
            spawns: Mutex::new(HashMap::new()),
        }),
    };

    let _ = SERVER.set(server.clone());

    tracing::info!("askpass: server listening at {:?}", socket_path);

    std::thread::Builder::new()
        .name("gitte-askpass".to_string())
        .spawn(move || {
            let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
                Ok(rt) => rt,

                Err(e) => {
                    tracing::error!("askpass: tokio runtime build failed: {e}");
                    return;
                }
            };

            runtime.block_on(async move {
                let listener = match UnixListener::from_std(std_listener) {
                    Ok(l) => l,

                    Err(e) => {
                        tracing::error!("askpass: tokio UnixListener wrap failed: {e}");
                        return;
                    }
                };

                serve(listener, server).await;
            });

            tracing::warn!("askpass: serve loop exited");
        })
        .ok();

    Some(Guard { socket_path })
}

pub fn current() -> Option<Server> {
    SERVER.get().cloned()
}

impl Server {
    pub fn register_spawn(&self) -> Result<String, std::fmt::Error> {
        let mut bytes = [0u8; 16];
        rand::fill(&mut bytes);

        let token = hex_encode(&bytes)?;

        self.inner
            .spawns
            .lock()
            .unwrap()
            .insert(token.clone(), SpawnContext::default());

        Ok(token)
    }

    pub fn release_spawn(&self, token: &str) {
        self.inner.spawns.lock().unwrap().remove(token);
    }

    pub fn socket_path(&self) -> &std::path::Path {
        &self.inner.socket_path
    }
}

fn bind_std_listener(socket_path: &std::path::Path) -> std::io::Result<StdUnixListener> {
    let std_listener = StdUnixListener::bind(socket_path)?;

    std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o600))?;
    std_listener.set_nonblocking(true)?;

    Ok(std_listener)
}

fn set_dir_mode_0700(dir: &std::path::Path) {
    if let Ok(meta) = std::fs::metadata(dir) {
        let mut perms = meta.permissions();
        perms.set_mode(0o700);

        let _ = std::fs::set_permissions(dir, perms);
    }
}

fn cleanup_stale_sockets(dir: &std::path::Path) {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return;
    };

    for entry in entries.flatten() {
        let path = entry.path();
        let name = entry.file_name();

        let Some(name) = name.to_str() else { continue };

        if !name.starts_with("askpass-") || !name.ends_with(".sock") {
            continue;
        }

        if is_stale_socket(&path) {
            let _ = std::fs::remove_file(&path);
        }
    }
}

fn is_stale_socket(path: &std::path::Path) -> bool {
    match UnixStream::connect(path) {
        Ok(_) => false,
        Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => true,
        Err(_) => false,
    }
}

async fn serve(listener: UnixListener, server: Server) {
    loop {
        let (stream, _addr) = match listener.accept().await {
            Ok(v) => v,

            Err(e) => {
                tracing::warn!("askpass: accept error: {e}");
                continue;
            }
        };

        tracing::info!("askpass: connection accepted");
        let server = server.clone();

        tokio::spawn(async move {
            if let Err(e) = handle_connection(stream, server).await {
                tracing::warn!("askpass: connection error: {e}");
            }
        });
    }
}

async fn handle_connection(stream: tokio::net::UnixStream, server: Server) -> std::io::Result<()> {
    let (reader, mut writer) = stream.into_split();
    let mut reader = BufReader::new(reader);
    let mut line = String::new();

    let n = tokio::time::timeout(Duration::from_secs(60), reader.read_line(&mut line))
        .await
        .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "askpass request timeout"))??;

    if n == 0 {
        return Ok(());
    }

    let req: AskpassRequest = match serde_json::from_str(line.trim_end()) {
        Ok(r) => r,

        Err(e) => {
            tracing::debug!("askpass: invalid request: {e}");
            return write_reply(&mut writer, &AskpassReply::Cancel).await;
        }
    };

    let token_known = server.inner.spawns.lock().unwrap().contains_key(&req.token);
    if !token_known {
        tracing::warn!("askpass: unknown or expired token");
        return write_reply(&mut writer, &AskpassReply::Cancel).await;
    }

    let prompt = CredentialPrompt::parse(&req.prompt);
    tracing::info!("askpass: parsed prompt: {:?}", prompt);
    let reply = resolve_prompt(&server, &req.token, prompt).await;

    tracing::info!(
        "askpass: replying {}",
        match &reply {
            AskpassReply::Ok { .. } => "ok",
            AskpassReply::Cancel => "cancel",
        }
    );

    write_reply(&mut writer, &reply).await
}

async fn resolve_prompt(server: &Server, token: &str, prompt: CredentialPrompt) -> AskpassReply {
    match &prompt {
        CredentialPrompt::Username { url } => {
            if let Some(cred) = lookup(url, None).await {
                remember_username(server, token, &cred.username);
                return AskpassReply::Ok { value: cred.username };
            }
        }

        CredentialPrompt::Password { url, username } => {
            let effective_user = username.clone().or_else(|| last_username(server, token));

            if let Some(user) = effective_user.as_deref() {
                remember_username(server, token, user);

                if let Some(cred) = lookup(url, Some(user)).await {
                    return AskpassReply::Ok { value: cred.password };
                }
            }
        }

        CredentialPrompt::Other { .. } => {}
    }

    let (tx, rx) = oneshot::channel();
    let prompt_for_dialog = prompt.clone();

    glib::idle_add_once(move || {
        credential_dialog::open(prompt_for_dialog, tx);
    });

    let answer = match rx.await {
        Ok(Some(a)) => a,
        _ => return AskpassReply::Cancel,
    };

    match prompt {
        CredentialPrompt::Username { url } => {
            let Some(username) = answer.username.clone() else {
                return AskpassReply::Cancel;
            };

            remember_username(server, token, &username);

            if let Some(password) = answer.password.as_deref() {
                persist(&url, &username, password).await;
            }

            AskpassReply::Ok { value: username }
        }

        CredentialPrompt::Password { url, username } => {
            let Some(password) = answer.password else {
                return AskpassReply::Cancel;
            };

            let user = username.or(answer.username);

            if let Some(user) = user.as_deref() {
                remember_username(server, token, user);
                persist(&url, user, &password).await;
            }

            AskpassReply::Ok { value: password }
        }

        CredentialPrompt::Other { .. } => match answer.password {
            Some(password) => AskpassReply::Ok { value: password },
            None => AskpassReply::Cancel,
        },
    }
}

fn last_username(server: &Server, token: &str) -> Option<String> {
    server
        .inner
        .spawns
        .lock()
        .unwrap()
        .get(token)
        .and_then(|c| c.last_username.clone())
}

fn remember_username(server: &Server, token: &str, user: &str) {
    if let Some(ctx) = server.inner.spawns.lock().unwrap().get_mut(token) {
        ctx.last_username = Some(user.to_string());
    }
}

async fn persist(url: &str, user: &str, password: &str) {
    match CredentialStore::load().await {
        Ok(store) => {
            if let Err(e) = store.store(url, user, password).await {
                tracing::warn!("askpass: keyring store failed: {e}");
            }
        }

        Err(e) => tracing::warn!("askpass: keyring load failed: {e}"),
    }
}

async fn lookup(url: &str, user: Option<&str>) -> Option<crate::credentials::Credential> {
    match CredentialStore::load().await {
        Ok(store) => match store.lookup(url, user).await {
            Ok(c) => c,

            Err(e) => {
                tracing::warn!("askpass: keyring lookup failed: {e}");
                None
            }
        },

        Err(e) => {
            tracing::warn!("askpass: keyring load failed: {e}");
            None
        }
    }
}

async fn write_reply(writer: &mut tokio::net::unix::OwnedWriteHalf, reply: &AskpassReply) -> std::io::Result<()> {
    let json = serde_json::to_string(reply).unwrap_or_else(|_| "{\"kind\":\"cancel\"}".to_string());
    writer.write_all(json.as_bytes()).await?;
    writer.write_all(b"\n").await?;

    writer.shutdown().await
}

fn hex_encode(bytes: &[u8]) -> Result<String, std::fmt::Error> {
    let mut s = String::with_capacity(bytes.len() * 2);

    for b in bytes {
        write!(s, "{b:02x}")?;
    }

    Ok(s)
}
