use super::{AudioSide, VoiceEngine, VoiceName, VoiceSelection};
use std::ffi::c_void;
use std::fmt;
use std::mem::ManuallyDrop;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::ptr::{self, NonNull};

use windows_sys::Win32::Foundation::{SysAllocString, SysFreeString, SysStringLen, VARIANT_FALSE};
use windows_sys::Win32::Globalization::{LCIDToLocaleName, LOCALE_USER_DEFAULT};
use windows_sys::Win32::System::Com::{
    CLSCTX_ALL, CLSIDFromProgID, COINIT_APARTMENTTHREADED, CoCreateInstance, CoInitializeEx,
    CoUninitialize, DISPATCH_METHOD, DISPATCH_PROPERTYGET, DISPATCH_PROPERTYPUTREF, DISPPARAMS,
    EXCEPINFO,
};
use windows_sys::Win32::System::Ole::DISPID_PROPERTYPUT;
use windows_sys::Win32::System::Variant::{
    VARIANT, VT_BOOL, VT_BSTR, VT_DISPATCH, VT_EMPTY, VT_I4, VariantClear, VariantInit,
};
use windows_sys::core::{BSTR, GUID, HRESULT, PCWSTR};

const IID_IDISPATCH: GUID = GUID::from_u128(0x00020400_0000_0000_c000_000000000046);
const IID_NULL: GUID = GUID::from_u128(0);
const SAPI_CREATE_FOR_WRITE: i32 = 3;
const SVSF_DEFAULT: i32 = 0;

#[repr(C)]
struct IDispatch {
    vtbl: *const IDispatchVtbl,
}

// COM exposes IDispatch through an unsafe vtable. All calls are kept inside
// ComDispatch so pointer validity, AddRef/Release ownership, and HRESULT
// checking are centralised.
#[repr(C)]
struct IDispatchVtbl {
    query_interface: unsafe extern "system" fn(
        this: *mut IDispatch,
        riid: *const GUID,
        ppv: *mut *mut c_void,
    ) -> HRESULT,
    add_ref: unsafe extern "system" fn(this: *mut IDispatch) -> u32,
    release: unsafe extern "system" fn(this: *mut IDispatch) -> u32,
    get_type_info_count:
        unsafe extern "system" fn(this: *mut IDispatch, pctinfo: *mut u32) -> HRESULT,
    get_type_info: unsafe extern "system" fn(
        this: *mut IDispatch,
        itinfo: u32,
        lcid: u32,
        pptinfo: *mut *mut c_void,
    ) -> HRESULT,
    get_ids_of_names: unsafe extern "system" fn(
        this: *mut IDispatch,
        riid: *const GUID,
        rgsznames: *mut PCWSTR,
        cnames: u32,
        lcid: u32,
        rgdispid: *mut i32,
    ) -> HRESULT,
    invoke: unsafe extern "system" fn(
        this: *mut IDispatch,
        dispidmember: i32,
        riid: *const GUID,
        lcid: u32,
        wflags: u16,
        pdispparams: *mut DISPPARAMS,
        pvarresult: *mut VARIANT,
        pexcepinfo: *mut EXCEPINFO,
        puargerr: *mut u32,
    ) -> HRESULT,
}

pub(crate) struct SapiSynthesizer {
    voice: ComDispatch,
    prompt_voice: Option<ComDispatch>,
    response_voice: Option<ComDispatch>,
    _com: ComApartment,
}

impl SapiSynthesizer {
    pub(crate) fn new(voices: &VoiceSelection) -> Result<Self, SapiError> {
        let _com = ComApartment::new()?;
        let voice = ComDispatch::create("SAPI.SpVoice")?;
        let prompt_voice = voices
            .filter_for_engine_side(AudioSide::Prompt, VoiceEngine::Sapi)
            .map(|filter| find_voice_token(&voice, filter))
            .transpose()?;
        let response_voice = voices
            .filter_for_engine_side(AudioSide::Response, VoiceEngine::Sapi)
            .map(|filter| find_voice_token(&voice, filter))
            .transpose()?;
        Ok(Self {
            voice,
            prompt_voice,
            response_voice,
            _com,
        })
    }

    pub(crate) fn voice_names() -> Result<Vec<VoiceName>, SapiError> {
        let _com = ComApartment::new()?;
        let voice = ComDispatch::create("SAPI.SpVoice")?;
        let voices = voice.invoke_method("GetVoices", &mut [])?.into_dispatch()?;
        let count = voices.property_get("Count")?.as_i32()? as usize;
        let mut names = Vec::with_capacity(count);
        for index in 0..count {
            let token = voices
                .invoke_method("Item", &mut [VariantArg::i4(index as i32)])?
                .into_dispatch()?;
            let description = token
                .invoke_method("GetDescription", &mut [])?
                .as_string()?;
            names.push(VoiceName::new(
                voice_label(&description, token_language_code(&token)),
                description,
            ));
        }
        Ok(names)
    }

    pub(crate) fn speak_to_wav(
        &self,
        side: AudioSide,
        text: &str,
        path: &Path,
    ) -> Result<(), SapiError> {
        let stream = ComDispatch::create("SAPI.SpFileStream")?;
        let path_text = path_to_string(path)?;
        if let Some(side_voice) = self.voice_for_side(side) {
            self.voice.property_put_dispatch("Voice", side_voice)?;
        }
        stream.invoke_method(
            "Open",
            &mut [
                VariantArg::bstr(&path_text)?,
                VariantArg::i4(SAPI_CREATE_FOR_WRITE),
                VariantArg::bool(false),
            ],
        )?;
        self.voice
            .property_put_dispatch("AudioOutputStream", &stream)?;
        let speak_result = self.voice.invoke_method(
            "Speak",
            &mut [VariantArg::bstr(text)?, VariantArg::i4(SVSF_DEFAULT)],
        );
        let close_result = stream.invoke_method("Close", &mut []);

        speak_result?;
        close_result?;
        Ok(())
    }

    fn voice_for_side(&self, side: AudioSide) -> Option<&ComDispatch> {
        match side {
            AudioSide::Prompt => self.prompt_voice.as_ref(),
            AudioSide::Response => self.response_voice.as_ref(),
        }
    }
}

fn find_voice_token(voice: &ComDispatch, filter: &str) -> Result<ComDispatch, SapiError> {
    let filter = filter.to_lowercase();
    let voices = voice.invoke_method("GetVoices", &mut [])?.into_dispatch()?;
    let count = voices.property_get("Count")?.as_i32()? as usize;
    for index in 0..count {
        let token = voices
            .invoke_method("Item", &mut [VariantArg::i4(index as i32)])?
            .into_dispatch()?;
        let name = token
            .invoke_method("GetDescription", &mut [])?
            .as_string()?;
        if name.to_lowercase().contains(&filter) {
            return Ok(token);
        }
    }

    Err(SapiError::VoiceNotFound(filter))
}

fn voice_label(description: &str, language: Option<String>) -> String {
    match language {
        Some(language) if !language.is_empty() => format!("{description} [{language}]"),
        _ => description.to_string(),
    }
}

fn token_language_code(token: &ComDispatch) -> Option<String> {
    let mut attribute_arg = VariantArg::bstr("Language").ok()?;
    let language = token
        .invoke_method("GetAttribute", std::slice::from_mut(&mut attribute_arg))
        .ok()?
        .as_string()
        .ok()?;
    language
        .split(';')
        .map(str::trim)
        .find(|value| !value.is_empty())
        .and_then(locale_name_from_sapi_language)
}

fn locale_name_from_sapi_language(value: &str) -> Option<String> {
    let hex = value
        .strip_prefix("0x")
        .or_else(|| value.strip_prefix("0X"))
        .unwrap_or(value);
    let lcid = u32::from_str_radix(hex, 16).ok()?;
    let mut buffer = [0_u16; 85];
    // SAFETY: buffer is a valid writable UTF-16 buffer. LCIDToLocaleName
    // writes at most buffer.len() elements and does not retain the pointer.
    let len = unsafe { LCIDToLocaleName(lcid, buffer.as_mut_ptr(), buffer.len() as i32, 0) };
    if len <= 1 {
        return None;
    }
    Some(String::from_utf16_lossy(&buffer[..len as usize - 1]))
}

struct ComApartment;

impl ComApartment {
    fn new() -> Result<Self, SapiError> {
        // SAFETY: Initialises COM for the current thread. The ComApartment
        // RAII guard calls CoUninitialize on the same thread when dropped.
        let result = unsafe { CoInitializeEx(ptr::null(), COINIT_APARTMENTTHREADED as u32) };
        check_hresult("CoInitializeEx", result)?;
        Ok(Self)
    }
}

impl Drop for ComApartment {
    fn drop(&mut self) {
        // SAFETY: This balances the successful CoInitializeEx represented
        // by this ComApartment value.
        unsafe {
            CoUninitialize();
        }
    }
}

struct ComDispatch {
    ptr: NonNull<IDispatch>,
}

impl ComDispatch {
    fn create(prog_id: &str) -> Result<Self, SapiError> {
        let mut clsid = GUID::default();
        let prog_id = wide_null(prog_id);
        // SAFETY: prog_id is NUL-terminated UTF-16 and clsid points to
        // valid writable storage for the returned CLSID.
        check_hresult("CLSIDFromProgID", unsafe {
            CLSIDFromProgID(prog_id.as_ptr(), &mut clsid)
        })?;

        let mut raw = ptr::null_mut();
        // SAFETY: clsid is initialised by CLSIDFromProgID, the requested
        // interface IID is IDispatch, and raw points to writable storage for
        // the returned COM interface pointer. Ownership is transferred into
        // ComDispatch::from_raw on success.
        check_hresult("CoCreateInstance", unsafe {
            CoCreateInstance(
                &clsid,
                ptr::null_mut(),
                CLSCTX_ALL,
                &IID_IDISPATCH,
                &mut raw,
            )
        })?;
        Self::from_raw(raw.cast())
    }

    fn from_raw(ptr: *mut IDispatch) -> Result<Self, SapiError> {
        Ok(Self {
            ptr: NonNull::new(ptr).ok_or(SapiError::NullComPointer)?,
        })
    }

    fn dispatch_id(&self, name: &str) -> Result<i32, SapiError> {
        let wide_name = wide_null(name);
        let mut name_ptr = wide_name.as_ptr();
        let mut dispid = 0;
        // SAFETY: self.ptr is a non-null live IDispatch pointer owned by
        // ComDispatch. wide_name and name_ptr remain valid for the call,
        // and dispid points to writable storage for the result.
        check_hresult(name, unsafe {
            ((*self.ptr.as_ref().vtbl).get_ids_of_names)(
                self.ptr.as_ptr(),
                &IID_NULL,
                &mut name_ptr,
                1,
                LOCALE_USER_DEFAULT,
                &mut dispid,
            )
        })?;
        Ok(dispid)
    }

    fn invoke_method(
        &self,
        name: &str,
        args: &mut [VariantArg],
    ) -> Result<VariantResult, SapiError> {
        self.invoke(name, DISPATCH_METHOD, args, None)
    }

    fn property_get(&self, name: &str) -> Result<VariantResult, SapiError> {
        self.invoke(name, DISPATCH_PROPERTYGET, &mut [], None)
    }

    fn property_put_dispatch(&self, name: &str, dispatch: &ComDispatch) -> Result<(), SapiError> {
        self.invoke(
            name,
            DISPATCH_PROPERTYPUTREF,
            &mut [VariantArg::dispatch(dispatch)],
            Some(DISPID_PROPERTYPUT),
        )?;
        Ok(())
    }

    fn invoke(
        &self,
        name: &str,
        flags: u16,
        args: &mut [VariantArg],
        named_arg: Option<i32>,
    ) -> Result<VariantResult, SapiError> {
        let dispid = self.dispatch_id(name)?;
        args.reverse();
        let mut named_arg = named_arg.unwrap_or_default();
        let mut params = DISPPARAMS {
            rgvarg: if args.is_empty() {
                ptr::null_mut()
            } else {
                args.as_mut_ptr().cast()
            },
            rgdispidNamedArgs: if named_arg == 0 {
                ptr::null_mut()
            } else {
                &mut named_arg
            },
            cArgs: args.len() as u32,
            cNamedArgs: u32::from(named_arg != 0),
        };
        let mut result = VariantResult::empty();
        let mut excepinfo = EXCEPINFO::default();
        let mut arg_error = 0;
        // SAFETY: self.ptr is a live IDispatch pointer. DISPPARAMS points
        // either to null or to the mutable VariantArg slice, whose elements
        // remain alive for the call. result and excepinfo point to valid
        // writable storage and are cleared or consumed below.
        let hr = unsafe {
            ((*self.ptr.as_ref().vtbl).invoke)(
                self.ptr.as_ptr(),
                dispid,
                &IID_NULL,
                LOCALE_USER_DEFAULT,
                flags,
                &mut params,
                result.as_mut_ptr(),
                &mut excepinfo,
                &mut arg_error,
            )
        };
        args.reverse();

        if hr < 0 {
            // SAFETY: excepinfo was initialised for Invoke and any BSTRs it
            // contains are owned by the caller after a failed COM call.
            let detail = unsafe { excepinfo_message(&excepinfo) };
            // SAFETY: clear_excepinfo frees only BSTR fields from this
            // stack-owned EXCEPINFO once.
            unsafe { clear_excepinfo(excepinfo) };
            return Err(SapiError::ComCallFailed {
                operation: name.to_string(),
                hresult: hr,
                detail,
            });
        }

        // SAFETY: On success, EXCEPINFO may still contain optional BSTR
        // fields; freeing null fields is valid, and this value is not used
        // afterwards.
        unsafe { clear_excepinfo(excepinfo) };
        Ok(result)
    }
}

impl Drop for ComDispatch {
    fn drop(&mut self) {
        // SAFETY: ComDispatch owns one reference to a non-null IDispatch
        // pointer. Drop releases that reference exactly once.
        unsafe {
            ((*self.ptr.as_ref().vtbl).release)(self.ptr.as_ptr());
        }
    }
}

#[repr(transparent)]
struct VariantArg {
    inner: VARIANT,
}

impl VariantArg {
    fn empty() -> Self {
        let mut inner = VARIANT::default();
        // SAFETY: inner points to valid writable VARIANT storage.
        unsafe {
            VariantInit(&mut inner);
        }
        Self { inner }
    }

    fn i4(value: i32) -> Self {
        let mut arg = Self::empty();
        arg.inner.Anonymous.Anonymous.vt = VT_I4;
        arg.inner.Anonymous.Anonymous.Anonymous.lVal = value;
        arg
    }

    fn bool(value: bool) -> Self {
        let mut arg = Self::empty();
        arg.inner.Anonymous.Anonymous.vt = VT_BOOL;
        arg.inner.Anonymous.Anonymous.Anonymous.boolVal = if value { -1 } else { VARIANT_FALSE };
        arg
    }

    fn bstr(value: &str) -> Result<Self, SapiError> {
        let mut arg = Self::empty();
        let wide = wide_null(value);
        // SAFETY: wide is NUL-terminated UTF-16. SysAllocString copies the
        // string into a new BSTR owned by the VARIANT below.
        let bstr = unsafe { SysAllocString(wide.as_ptr()) };
        if bstr.is_null() && !value.is_empty() {
            return Err(SapiError::StringAllocation);
        }
        arg.inner.Anonymous.Anonymous.vt = VT_BSTR;
        arg.inner.Anonymous.Anonymous.Anonymous.bstrVal = bstr;
        Ok(arg)
    }

    fn dispatch(dispatch: &ComDispatch) -> Self {
        let mut arg = Self::empty();
        // SAFETY: dispatch.ptr is a live COM interface pointer. AddRef gives
        // the VARIANT its own reference, later released by VariantClear.
        unsafe {
            ((*dispatch.ptr.as_ref().vtbl).add_ref)(dispatch.ptr.as_ptr());
        }
        arg.inner.Anonymous.Anonymous.vt = VT_DISPATCH;
        arg.inner.Anonymous.Anonymous.Anonymous.pdispVal = dispatch.ptr.as_ptr().cast::<c_void>();
        arg
    }
}

impl Drop for VariantArg {
    fn drop(&mut self) {
        // SAFETY: inner is an initialised VARIANT owned by this wrapper.
        // VariantClear releases any BSTR/dispatch contents exactly once.
        unsafe {
            VariantClear(&mut self.inner);
        }
    }
}

struct VariantResult {
    inner: VARIANT,
}

impl VariantResult {
    fn empty() -> Self {
        let mut inner = VARIANT::default();
        // SAFETY: inner points to valid writable VARIANT storage.
        unsafe {
            VariantInit(&mut inner);
        }
        Self { inner }
    }

    fn as_mut_ptr(&mut self) -> *mut VARIANT {
        &mut self.inner
    }

    fn variant_type(&self) -> u16 {
        // SAFETY: inner is an initialised VARIANT and reading vt is valid
        // for every VARIANT type.
        unsafe { self.inner.Anonymous.Anonymous.vt }
    }

    fn as_i32(&self) -> Result<i32, SapiError> {
        if self.variant_type() != VT_I4 {
            return Err(SapiError::UnexpectedVariantType {
                expected: "VT_I4",
                actual: self.variant_type(),
            });
        }
        // SAFETY: variant_type verified that the active VARIANT member is
        // VT_I4, so reading lVal is valid.
        Ok(unsafe { self.inner.Anonymous.Anonymous.Anonymous.lVal })
    }

    fn as_string(&self) -> Result<String, SapiError> {
        if self.variant_type() != VT_BSTR {
            return Err(SapiError::UnexpectedVariantType {
                expected: "VT_BSTR",
                actual: self.variant_type(),
            });
        }
        // SAFETY: variant_type verified VT_BSTR, so reading bstrVal is
        // valid. bstr_to_string borrows the BSTR without taking ownership.
        let bstr = unsafe { self.inner.Anonymous.Anonymous.Anonymous.bstrVal };
        // SAFETY: bstr is the active BSTR member of this VARIANT.
        Ok(unsafe { bstr_to_string(bstr) })
    }

    fn into_dispatch(self) -> Result<ComDispatch, SapiError> {
        if self.variant_type() != VT_DISPATCH {
            return Err(SapiError::UnexpectedVariantType {
                expected: "VT_DISPATCH",
                actual: self.variant_type(),
            });
        }
        let mut this = ManuallyDrop::new(self);
        // SAFETY: variant_type verified VT_DISPATCH, so pdispVal is the
        // active member. Ownership of the COM reference is transferred to
        // ComDispatch and the VARIANT is neutralised to avoid VariantClear
        // releasing that same reference.
        let raw = unsafe { this.inner.Anonymous.Anonymous.Anonymous.pdispVal };
        this.inner.Anonymous.Anonymous.vt = VT_EMPTY;
        this.inner.Anonymous.Anonymous.Anonymous.pdispVal = ptr::null_mut();
        ComDispatch::from_raw(raw.cast())
    }
}

impl Drop for VariantResult {
    fn drop(&mut self) {
        // SAFETY: inner is an initialised VARIANT still owned by this
        // wrapper unless into_dispatch used ManuallyDrop to take ownership.
        unsafe {
            VariantClear(&mut self.inner);
        }
    }
}

fn check_hresult(operation: &str, hresult: HRESULT) -> Result<(), SapiError> {
    if hresult < 0 {
        Err(SapiError::ComCallFailed {
            operation: operation.to_string(),
            hresult,
            detail: None,
        })
    } else {
        Ok(())
    }
}

unsafe fn excepinfo_message(excepinfo: &EXCEPINFO) -> Option<String> {
    // SAFETY: Caller guarantees excepinfo is a valid COM EXCEPINFO whose
    // BSTR fields may be read before clear_excepinfo frees them.
    let description = unsafe { bstr_to_string(excepinfo.bstrDescription) };
    if description.is_empty() {
        None
    } else {
        Some(description)
    }
}

unsafe fn clear_excepinfo(excepinfo: EXCEPINFO) {
    // SAFETY: Caller transfers this EXCEPINFO by value and promises these
    // BSTR fields have not already been freed. SysFreeString accepts null.
    unsafe {
        SysFreeString(excepinfo.bstrSource);
        SysFreeString(excepinfo.bstrDescription);
        SysFreeString(excepinfo.bstrHelpFile);
    }
}

unsafe fn bstr_to_string(bstr: BSTR) -> String {
    if bstr.is_null() {
        return String::new();
    }
    // SAFETY: Caller guarantees bstr is a valid BSTR. SysStringLen returns
    // its UTF-16 length excluding the terminator.
    let len = unsafe { SysStringLen(bstr) } as usize;
    // SAFETY: BSTR storage contains at least len UTF-16 code units.
    let slice = unsafe { std::slice::from_raw_parts(bstr, len) };
    String::from_utf16_lossy(slice)
}

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

fn path_to_string(path: &Path) -> Result<String, SapiError> {
    path.as_os_str()
        .encode_wide()
        .chain(std::iter::once(0))
        .collect::<Vec<_>>()
        .split_last()
        .map(|(_, units)| String::from_utf16_lossy(units))
        .ok_or(SapiError::InvalidPath)
}

#[derive(Debug)]
pub(crate) enum SapiError {
    ComCallFailed {
        operation: String,
        hresult: HRESULT,
        detail: Option<String>,
    },
    NullComPointer,
    StringAllocation,
    InvalidPath,
    UnexpectedVariantType {
        expected: &'static str,
        actual: u16,
    },
    VoiceNotFound(String),
}

impl fmt::Display for SapiError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ComCallFailed {
                operation,
                hresult,
                detail,
            } => {
                write!(
                    f,
                    "{operation} failed with HRESULT 0x{:08x}",
                    *hresult as u32
                )?;
                if let Some(detail) = detail {
                    write!(f, ": {detail}")?;
                }
                Ok(())
            }
            Self::NullComPointer => f.write_str("COM returned a null pointer"),
            Self::StringAllocation => f.write_str("could not allocate COM string"),
            Self::InvalidPath => f.write_str("path could not be converted for SAPI"),
            Self::UnexpectedVariantType { expected, actual } => {
                write!(
                    f,
                    "expected {expected} from SAPI, got variant type {actual}"
                )
            }
            Self::VoiceNotFound(filter) => {
                write!(f, "no installed SAPI voice matched {filter:?}")
            }
        }
    }
}

impl std::error::Error for SapiError {}
