// SPDX-License-Identifier: AGPL-3.0-or-later

use std::ffi::OsString;
use std::os::windows::ffi::OsStrExt;

use napi::Result;
use windows::Win32::Foundation::{HWND, LPARAM};
use windows::Win32::Networking::WindowsWebServices::{
    WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM, WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM,
    WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS,
    WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION,
    WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS,
    WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_CURRENT_VERSION, WEBAUTHN_CLIENT_DATA,
    WEBAUTHN_CLIENT_DATA_CURRENT_VERSION, WEBAUTHN_COSE_CREDENTIAL_PARAMETER,
    WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION, WEBAUTHN_COSE_CREDENTIAL_PARAMETERS,
    WEBAUTHN_CREDENTIAL, WEBAUTHN_CREDENTIAL_CURRENT_VERSION, WEBAUTHN_CREDENTIALS,
    WEBAUTHN_CTAP_TRANSPORT_INTERNAL, WEBAUTHN_CTAP_TRANSPORT_USB, WEBAUTHN_HASH_ALGORITHM_SHA_256,
    WEBAUTHN_LARGE_BLOB_SUPPORT_NONE, WEBAUTHN_RP_ENTITY_INFORMATION,
    WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION, WEBAUTHN_USER_ENTITY_INFORMATION,
    WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION, WebAuthNAuthenticatorGetAssertion,
    WebAuthNAuthenticatorMakeCredential, WebAuthNFreeAssertion, WebAuthNFreeCredentialAttestation,
    WebAuthNGetApiVersionNumber, WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable,
};
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::UI::WindowsAndMessaging::{
    EnumWindows, GetDesktopWindow, GetForegroundWindow, GetWindowThreadProcessId, IsWindow,
    IsWindowVisible,
};
use windows_core::{BOOL, PCWSTR};

use crate::common::{
    CREATE_PREFIX, CreateInput, CreateResult, DescriptorInput, GET_PREFIX, GetInput, GetResult,
    ceremony_error,
};

pub fn api_version() -> u32 {
    unsafe { WebAuthNGetApiVersionNumber() }
}

pub fn is_user_verifying_platform_authenticator_available() -> bool {
    match unsafe { WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable() } {
        Ok(b) => b.as_bool(),
        Err(_) => false,
    }
}

fn to_wide(value: &str) -> Vec<u16> {
    OsString::from(value).encode_wide().chain(Some(0)).collect()
}

fn window_from_handle(handle: u64) -> Option<HWND> {
    if handle == 0 {
        return None;
    }
    let hwnd = HWND(handle as usize as *mut core::ffi::c_void);
    if unsafe { IsWindow(Some(hwnd)) }.as_bool() {
        Some(hwnd)
    } else {
        None
    }
}

fn window_process_id(hwnd: HWND) -> u32 {
    let mut pid = 0u32;
    unsafe { GetWindowThreadProcessId(hwnd, Some(&mut pid)) };
    pid
}

unsafe extern "system" fn collect_own_visible_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
    let slot = unsafe { &mut *(lparam.0 as *mut Option<HWND>) };
    if window_process_id(hwnd) == unsafe { GetCurrentProcessId() }
        && unsafe { IsWindowVisible(hwnd) }.as_bool()
    {
        *slot = Some(hwnd);
        return BOOL(0);
    }
    BOOL(1)
}

fn interaction_window(handle: u64) -> HWND {
    if let Some(hwnd) = window_from_handle(handle) {
        return hwnd;
    }
    let current_pid = unsafe { GetCurrentProcessId() };
    let foreground = unsafe { GetForegroundWindow() };
    if !foreground.is_invalid() && window_process_id(foreground) == current_pid {
        return foreground;
    }
    let mut own_window: Option<HWND> = None;
    let _ = unsafe {
        EnumWindows(
            Some(collect_own_visible_window),
            LPARAM(&raw mut own_window as isize),
        )
    };
    if let Some(hwnd) = own_window {
        return hwnd;
    }
    if !foreground.is_invalid() {
        return foreground;
    }
    unsafe { GetDesktopWindow() }
}

const PUBLIC_KEY_WIDE: &[u16] = &[
    'p' as u16, 'u' as u16, 'b' as u16, 'l' as u16, 'i' as u16, 'c' as u16, '-' as u16, 'k' as u16,
    'e' as u16, 'y' as u16, 0,
];

fn build_credential_list(
    descriptors: &[DescriptorInput],
) -> (Vec<WEBAUTHN_CREDENTIAL>, WEBAUTHN_CREDENTIALS) {
    let mut backing: Vec<WEBAUTHN_CREDENTIAL> = descriptors
        .iter()
        .map(|d| WEBAUTHN_CREDENTIAL {
            dwVersion: WEBAUTHN_CREDENTIAL_CURRENT_VERSION,
            cbId: d.id.len() as u32,
            pbId: d.id.as_ptr() as *mut u8,
            pwszCredentialType: PCWSTR(PUBLIC_KEY_WIDE.as_ptr()),
        })
        .collect();
    let list = WEBAUTHN_CREDENTIALS {
        cCredentials: backing.len() as u32,
        pCredentials: if backing.is_empty() {
            std::ptr::null_mut()
        } else {
            backing.as_mut_ptr()
        },
    };
    (backing, list)
}

fn copy_buffer(ptr: *const u8, len: u32) -> Result<Vec<u8>> {
    if len == 0 {
        return Ok(Vec::new());
    }
    if ptr.is_null() {
        return Err(ceremony_error(CREATE_PREFIX, "InvalidWebAuthnResult"));
    }

    let slice = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
    Ok(slice.to_vec())
}

pub fn make_credential(input: &mut CreateInput) -> Result<CreateResult> {
    let rp_id_w = to_wide(&input.rp_id);
    let rp_name_w = to_wide(&input.rp_name);
    let user_name_w = to_wide(&input.user_name);
    let display_w = to_wide(&input.user_display_name);

    let rp = WEBAUTHN_RP_ENTITY_INFORMATION {
        dwVersion: WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION,
        pwszId: PCWSTR(rp_id_w.as_ptr()),
        pwszName: PCWSTR(rp_name_w.as_ptr()),
        pwszIcon: PCWSTR::null(),
    };
    let user = WEBAUTHN_USER_ENTITY_INFORMATION {
        dwVersion: WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION,
        cbId: input.user_id.len() as u32,
        pbId: input.user_id.as_ptr() as *mut u8,
        pwszName: PCWSTR(user_name_w.as_ptr()),
        pwszIcon: PCWSTR::null(),
        pwszDisplayName: PCWSTR(display_w.as_ptr()),
    };

    let mut pub_key_params: Vec<WEBAUTHN_COSE_CREDENTIAL_PARAMETER> = input
        .pub_key_algs
        .iter()
        .map(|&alg| WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
            dwVersion: WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION,
            pwszCredentialType: PCWSTR(PUBLIC_KEY_WIDE.as_ptr()),
            lAlg: alg,
        })
        .collect();
    let params = WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
        cCredentialParameters: pub_key_params.len() as u32,
        pCredentialParameters: pub_key_params.as_mut_ptr(),
    };

    let client_data = WEBAUTHN_CLIENT_DATA {
        dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
        cbClientDataJSON: input.client_data_json.len() as u32,
        pbClientDataJSON: input.client_data_json.as_ptr() as *mut u8,
        pwszHashAlgId: WEBAUTHN_HASH_ALGORITHM_SHA_256,
    };

    let (_exclude_keep, exclude_list) = build_credential_list(&input.exclude_credentials);

    let options = WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS {
        dwVersion: api_version()
            .min(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_CURRENT_VERSION),
        dwTimeoutMilliseconds: input.timeout_ms,
        CredentialList: exclude_list,
        dwAuthenticatorAttachment: input.authenticator_attachment,
        bRequireResidentKey: BOOL(input.require_resident_key as i32),
        dwUserVerificationRequirement: input.user_verification,
        dwAttestationConveyancePreference: input.attestation,
        dwEnterpriseAttestation: input.enterprise_attestation,
        dwLargeBlobSupport: WEBAUTHN_LARGE_BLOB_SUPPORT_NONE,
        bPreferResidentKey: BOOL(input.prefer_resident_key as i32),
        ..Default::default()
    };

    let attestation_ptr = unsafe {
        WebAuthNAuthenticatorMakeCredential(
            interaction_window(input.window_handle),
            &rp,
            &user,
            &params,
            &client_data,
            Some(&options),
        )
    };
    let attestation_ptr = attestation_ptr
        .map_err(|err| ceremony_error(CREATE_PREFIX, &format!("WebAuthnCallFailed: {err}")))?;
    if attestation_ptr.is_null() {
        return Err(ceremony_error(CREATE_PREFIX, "InvalidWebAuthnResult"));
    }

    let att = unsafe { &*attestation_ptr };
    let raw_id = copy_buffer(att.pbCredentialId, att.cbCredentialId)?;
    let attestation_object = copy_buffer(att.pbAttestationObject, att.cbAttestationObject)?;
    let used_transport = if att.dwVersion >= 3 {
        att.dwUsedTransport
    } else {
        0
    };

    unsafe { WebAuthNFreeCredentialAttestation(Some(attestation_ptr)) };

    Ok(CreateResult {
        raw_id,
        attestation_object,
        client_data_json: input.client_data_json.clone(),
        used_transport,
    })
}

pub fn get_assertion(input: &mut GetInput) -> Result<GetResult> {
    let rp_id_w = to_wide(&input.rp_id);

    let client_data = WEBAUTHN_CLIENT_DATA {
        dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
        cbClientDataJSON: input.client_data_json.len() as u32,
        pbClientDataJSON: input.client_data_json.as_ptr() as *mut u8,
        pwszHashAlgId: WEBAUTHN_HASH_ALGORITHM_SHA_256,
    };

    let (_allow_keep, allow_list) = build_credential_list(&input.allow_credentials);

    let options = WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS {
        dwVersion: api_version().min(WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION),
        dwTimeoutMilliseconds: input.timeout_ms,
        CredentialList: allow_list,
        dwAuthenticatorAttachment: input.authenticator_attachment,
        dwUserVerificationRequirement: input.user_verification,
        ..Default::default()
    };

    let assertion_ptr = unsafe {
        WebAuthNAuthenticatorGetAssertion(
            interaction_window(input.window_handle),
            PCWSTR(rp_id_w.as_ptr()),
            &client_data,
            Some(&options),
        )
    };
    let assertion_ptr = assertion_ptr
        .map_err(|err| ceremony_error(GET_PREFIX, &format!("WebAuthnCallFailed: {err}")))?;
    if assertion_ptr.is_null() {
        return Err(ceremony_error(GET_PREFIX, "InvalidWebAuthnResult"));
    }

    let asrt = unsafe { &*assertion_ptr };
    let raw_id = copy_buffer(asrt.Credential.pbId, asrt.Credential.cbId)?;
    let authenticator_data = copy_buffer(asrt.pbAuthenticatorData, asrt.cbAuthenticatorData)?;
    let signature = copy_buffer(asrt.pbSignature, asrt.cbSignature)?;
    let user_handle = if asrt.cbUserId > 0 {
        Some(copy_buffer(asrt.pbUserId, asrt.cbUserId)?)
    } else {
        None
    };

    let used_transport = if asrt.dwVersion >= 4 {
        asrt.dwUsedTransport
    } else if input.authenticator_attachment == WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM {
        WEBAUTHN_CTAP_TRANSPORT_INTERNAL
    } else if input.authenticator_attachment == WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM {
        WEBAUTHN_CTAP_TRANSPORT_USB
    } else {
        0
    };

    unsafe { WebAuthNFreeAssertion(assertion_ptr) };

    Ok(GetResult {
        raw_id,
        authenticator_data,
        signature,
        user_handle,
        client_data_json: input.client_data_json.clone(),
        used_transport,
    })
}
