//! Best-effort OS keychain storage for session passwords.
//!
//! Windows and macOS use native system APIs. Linux uses `secret-tool` when
//! available (GNOME Keyring / Secret Service). Callers keep an encrypted config
//! fallback for machines without a usable keychain.

use anyhow::{anyhow, Context, Result};

const SERVICE: &str = "meatshell";

pub fn get_password(account: &str) -> Result<String> {
    platform::get_password(account)
}

pub fn set_password(account: &str, password: &str) -> Result<()> {
    platform::set_password(account, password)
}

#[cfg(windows)]
mod platform {
    use super::*;
    use std::ffi::c_void;
    use std::slice;

    const CRED_TYPE_GENERIC: u32 = 1;
    const CRED_PERSIST_LOCAL_MACHINE: u32 = 2;

    #[repr(C)]
    struct FileTime {
        low_date_time: u32,
        high_date_time: u32,
    }

    #[repr(C)]
    struct CredentialW {
        flags: u32,
        typ: u32,
        target_name: *mut u16,
        comment: *mut u16,
        last_written: FileTime,
        credential_blob_size: u32,
        credential_blob: *mut u8,
        persist: u32,
        attribute_count: u32,
        attributes: *mut c_void,
        target_alias: *mut u16,
        user_name: *mut u16,
    }

    #[link(name = "advapi32")]
    extern "system" {
        fn CredWriteW(credential: *const CredentialW, flags: u32) -> i32;
        fn CredReadW(
            target_name: *const u16,
            typ: u32,
            flags: u32,
            credential: *mut *mut CredentialW,
        ) -> i32;
        fn CredFree(buffer: *mut c_void);
    }

    pub fn set_password(account: &str, password: &str) -> Result<()> {
        let mut target = wide(&target(account));
        let mut user = wide(account);
        let mut blob = password.as_bytes().to_vec();
        let cred = CredentialW {
            flags: 0,
            typ: CRED_TYPE_GENERIC,
            target_name: target.as_mut_ptr(),
            comment: std::ptr::null_mut(),
            last_written: FileTime {
                low_date_time: 0,
                high_date_time: 0,
            },
            credential_blob_size: blob.len() as u32,
            credential_blob: blob.as_mut_ptr(),
            persist: CRED_PERSIST_LOCAL_MACHINE,
            attribute_count: 0,
            attributes: std::ptr::null_mut(),
            target_alias: std::ptr::null_mut(),
            user_name: user.as_mut_ptr(),
        };
        let ok = unsafe { CredWriteW(&cred, 0) };
        if ok == 0 {
            return Err(anyhow!("CredWriteW failed"));
        }
        Ok(())
    }

    pub fn get_password(account: &str) -> Result<String> {
        let target = wide(&target(account));
        let mut cred: *mut CredentialW = std::ptr::null_mut();
        let ok = unsafe { CredReadW(target.as_ptr(), CRED_TYPE_GENERIC, 0, &mut cred) };
        if ok == 0 || cred.is_null() {
            return Err(anyhow!("CredReadW failed"));
        }
        let result = unsafe {
            let c = &*cred;
            let bytes = slice::from_raw_parts(
                c.credential_blob,
                c.credential_blob_size as usize,
            );
            String::from_utf8(bytes.to_vec()).context("credential is not UTF-8")
        };
        unsafe { CredFree(cred.cast()) };
        result
    }

    fn target(account: &str) -> String {
        format!("{}:{}", SERVICE, account)
    }

    fn wide(s: &str) -> Vec<u16> {
        s.encode_utf16().chain(std::iter::once(0)).collect()
    }
}

#[cfg(target_os = "macos")]
mod platform {
    use super::*;
    use std::ffi::{c_char, c_void, CString};
    use std::slice;

    const ERR_SEC_SUCCESS: i32 = 0;
    const ERR_SEC_DUPLICATE_ITEM: i32 = -25299;

    #[link(name = "Security", kind = "framework")]
    extern "C" {
        fn SecKeychainAddGenericPassword(
            keychain: *mut c_void,
            service_name_length: u32,
            service_name: *const c_char,
            account_name_length: u32,
            account_name: *const c_char,
            password_length: u32,
            password_data: *const c_void,
            item_ref: *mut *mut c_void,
        ) -> i32;
        fn SecKeychainFindGenericPassword(
            keychain_or_array: *mut c_void,
            service_name_length: u32,
            service_name: *const c_char,
            account_name_length: u32,
            account_name: *const c_char,
            password_length: *mut u32,
            password_data: *mut *mut c_void,
            item_ref: *mut *mut c_void,
        ) -> i32;
        fn SecKeychainItemModifyContent(
            item_ref: *mut c_void,
            attr_list: *const c_void,
            length: u32,
            data: *const c_void,
        ) -> i32;
        fn SecKeychainItemFreeContent(attr_list: *mut c_void, data: *mut c_void) -> i32;
        fn CFRelease(cf: *const c_void);
    }

    pub fn set_password(account: &str, password: &str) -> Result<()> {
        let service = cstr(SERVICE)?;
        let account = cstr(account)?;
        let pass = password.as_bytes();
        let mut item: *mut c_void = std::ptr::null_mut();
        let status = unsafe {
            SecKeychainAddGenericPassword(
                std::ptr::null_mut(),
                SERVICE.len() as u32,
                service.as_ptr(),
                account.as_bytes().len() as u32,
                account.as_ptr(),
                pass.len() as u32,
                pass.as_ptr().cast(),
                &mut item,
            )
        };
        if status == ERR_SEC_SUCCESS {
            if !item.is_null() {
                unsafe { CFRelease(item.cast()) };
            }
            return Ok(());
        }
        if status != ERR_SEC_DUPLICATE_ITEM {
            return Err(anyhow!("SecKeychainAddGenericPassword failed: {status}"));
        }

        let item = find_item(
            service.as_ptr(),
            SERVICE.len(),
            account.as_ptr(),
            account.as_bytes().len(),
        )?;
        let status = unsafe {
            SecKeychainItemModifyContent(
                item,
                std::ptr::null(),
                pass.len() as u32,
                pass.as_ptr().cast(),
            )
        };
        unsafe { CFRelease(item.cast()) };
        if status != ERR_SEC_SUCCESS {
            return Err(anyhow!("SecKeychainItemModifyContent failed: {status}"));
        }
        Ok(())
    }

    pub fn get_password(account: &str) -> Result<String> {
        let service = cstr(SERVICE)?;
        let account = cstr(account)?;
        let mut len = 0u32;
        let mut data: *mut c_void = std::ptr::null_mut();
        let mut item: *mut c_void = std::ptr::null_mut();
        let status = unsafe {
            SecKeychainFindGenericPassword(
                std::ptr::null_mut(),
                SERVICE.len() as u32,
                service.as_ptr(),
                account.as_bytes().len() as u32,
                account.as_ptr(),
                &mut len,
                &mut data,
                &mut item,
            )
        };
        if status != ERR_SEC_SUCCESS {
            return Err(anyhow!("SecKeychainFindGenericPassword failed: {status}"));
        }
        let result = unsafe {
            let bytes = slice::from_raw_parts(data.cast::<u8>(), len as usize);
            String::from_utf8(bytes.to_vec()).context("credential is not UTF-8")
        };
        unsafe {
            SecKeychainItemFreeContent(std::ptr::null_mut(), data);
            if !item.is_null() {
                CFRelease(item.cast());
            }
        }
        result
    }

    fn find_item(
        service: *const c_char,
        service_len: usize,
        account: *const c_char,
        account_len: usize,
    ) -> Result<*mut c_void> {
        let mut item: *mut c_void = std::ptr::null_mut();
        let status = unsafe {
            SecKeychainFindGenericPassword(
                std::ptr::null_mut(),
                service_len as u32,
                service,
                account_len as u32,
                account,
                std::ptr::null_mut(),
                std::ptr::null_mut(),
                &mut item,
            )
        };
        if status != ERR_SEC_SUCCESS || item.is_null() {
            return Err(anyhow!("SecKeychainFindGenericPassword failed: {status}"));
        }
        Ok(item)
    }

    fn cstr(s: &str) -> Result<CString> {
        CString::new(s).context("credential string contains NUL")
    }
}

#[cfg(all(unix, not(target_os = "macos")))]
mod platform {
    use super::*;
    use std::io::Write;
    use std::process::{Command, Stdio};

    pub fn set_password(account: &str, password: &str) -> Result<()> {
        let mut child = Command::new("secret-tool")
            .args([
                "store",
                "--label",
                "meatshell session password",
                "app",
                SERVICE,
                "account",
                account,
            ])
            .stdin(Stdio::piped())
            .spawn()
            .context("secret-tool is not available")?;
        child
            .stdin
            .as_mut()
            .context("secret-tool stdin unavailable")?
            .write_all(password.as_bytes())
            .context("write password to secret-tool")?;
        let status = child.wait().context("wait for secret-tool")?;
        if !status.success() {
            return Err(anyhow!("secret-tool store failed"));
        }
        Ok(())
    }

    pub fn get_password(account: &str) -> Result<String> {
        let out = Command::new("secret-tool")
            .args(["lookup", "app", SERVICE, "account", account])
            .output()
            .context("secret-tool is not available")?;
        if !out.status.success() {
            return Err(anyhow!("secret-tool lookup failed"));
        }
        let mut s = String::from_utf8(out.stdout).context("credential is not UTF-8")?;
        while s.ends_with('\n') || s.ends_with('\r') {
            s.pop();
        }
        Ok(s)
    }
}

#[cfg(not(any(unix, windows)))]
mod platform {
    use super::*;

    pub fn set_password(_account: &str, _password: &str) -> Result<()> {
        Err(anyhow!("no keychain backend for this platform"))
    }

    pub fn get_password(_account: &str) -> Result<String> {
        Err(anyhow!("no keychain backend for this platform"))
    }
}
