use std::ffi::{CStr, CString};
use std::fs;
use std::os::raw::c_char;
use std::path::{Path, PathBuf};

use anyhow::{anyhow, Context, Result};
use libloading::{Library, Symbol};

#[repr(C)]
struct C2paSignedResult {
    signed_data: *mut u8,
    signed_data_len: usize,
    manifest_id: *mut c_char,
    manifest_json: *mut c_char,
    error_message: *mut c_char,
    /// Non-null for path-based signing variants (library ≥ v1.1.0); the library wrote the output here.
    output_path: *mut c_char,
}

type C2paSignContentFn = unsafe extern "C" fn(
    image_data: *const u8,
    image_data_len: usize,
    filename: *const c_char,
    roles_csv: *const c_char,
    tsa_url: *const c_char,
    account_id: *const c_char,
    user_id: *const c_char,
) -> *mut C2paSignedResult;

type C2paSignContentWithCawgIdentityFn = unsafe extern "C" fn(
    image_data: *const u8,
    image_data_len: usize,
    filename: *const c_char,
    roles_csv: *const c_char,
    cawg_training_mining_json: *const c_char,
    tsa_url: *const c_char,
    account_id: *const c_char,
    user_id: *const c_char,
) -> *mut C2paSignedResult;

type C2paSignContentFromPathFn = unsafe extern "C" fn(
    input_path: *const c_char,
    output_path: *const c_char,
    filename: *const c_char,
    roles_csv: *const c_char,
    tsa_url: *const c_char,
    account_id: *const c_char,
    user_id: *const c_char,
) -> *mut C2paSignedResult;

type C2paSignContentWithCawgIdentityFromPathFn = unsafe extern "C" fn(
    input_path: *const c_char,
    output_path: *const c_char,
    filename: *const c_char,
    roles_csv: *const c_char,
    cawg_training_mining_json: *const c_char,
    tsa_url: *const c_char,
    account_id: *const c_char,
    user_id: *const c_char,
) -> *mut C2paSignedResult;

type C2paFreeResultFn = unsafe extern "C" fn(result: *mut C2paSignedResult);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SigningImplementation {
    Standard,
    CawgIdentity,
    /// Heap-optimized: reads input and writes output directly via file paths.
    /// Preferred for large files and videos. Requires library ≥ v1.1.0.
    StandardFromPath,
    /// Heap-optimized CAWG identity signing via file paths. Requires library ≥ v1.1.0.
    CawgIdentityFromPath,
}

#[derive(Debug, Clone)]
pub struct SignImageRequest {
    pub input_path: PathBuf,
    pub output_folder: PathBuf,
    pub binary_path: PathBuf,
    pub account_id: String,
    pub user_id: String,
    pub api_key: String,
    pub signing_service_url: String,
    pub tsa_url: String,
    /// Comma-separated CAWG roles passed to the signing library, e.g. `"creator,publisher"`.
    /// The library resolves actual display names from the signing service.
    pub roles_csv: String,
    pub skip_ssl_validation: bool,
    pub skip_c2pa_public_trust_list_check: bool,
    pub signing_implementation: SigningImplementation,
    pub cawg_training_mining_json: Option<String>,
    pub smime_credential_id: Option<String>,
    pub user_pin: Option<String>,
}

#[derive(Debug, Clone)]
pub struct SignImageResponse {
    pub output_path: PathBuf,
    pub output_size: usize,
    pub manifest_id: Option<String>,
}

impl SignImageRequest {
    pub fn with_defaults(
        input_path: impl Into<PathBuf>,
        output_folder: impl Into<PathBuf>,
        binary_path: impl Into<PathBuf>,
        account_id: impl Into<String>,
        api_key: impl Into<String>,
        signing_service_url: impl Into<String>,
    ) -> Self {
        Self {
            input_path: input_path.into(),
            output_folder: output_folder.into(),
            binary_path: binary_path.into(),
            account_id: account_id.into(),
            user_id: String::new(),
            api_key: api_key.into(),
            signing_service_url: signing_service_url.into(),
            tsa_url: "http://timestamp.digicert.com".to_string(),
            roles_csv: "creator,publisher".to_string(),
            skip_ssl_validation: true,
            skip_c2pa_public_trust_list_check: false,
            signing_implementation: SigningImplementation::Standard,
            cawg_training_mining_json: None,
            smime_credential_id: None,
            user_pin: None,
        }
    }
}

pub fn sign_image_file(mut request: SignImageRequest) -> Result<SignImageResponse> {
    sanitize_request(&mut request)?;

    if !request.input_path.exists() {
        return Err(anyhow!(
            "Input image not found: {}",
            request.input_path.display()
        ));
    }

    if !request.binary_path.exists() {
        return Err(anyhow!(
            "Dynamic library not found at '{}'. Build/copy the file into rust_binary first.",
            request.binary_path.display()
        ));
    }

    if !request.output_folder.exists() {
        fs::create_dir_all(&request.output_folder).with_context(|| {
            format!(
                "Failed to create output directory: {}",
                request.output_folder.display()
            )
        })?;
    }

    // For path-based variants the library reads the file itself; skip the in-memory load.
    let image_data = match request.signing_implementation {
        SigningImplementation::Standard | SigningImplementation::CawgIdentity => Some(
            fs::read(&request.input_path).with_context(|| {
                format!("Failed to read input image: {}", request.input_path.display())
            })?,
        ),
        _ => None,
    };

    let output_path = signed_output_path(
        &request.input_path,
        &request.output_folder,
        request.signing_implementation,
    )?;

    let c_filename = to_cstring(file_name(&request.input_path)?, "filename")?;
    let c_account = to_cstring(&request.account_id, "account_id")?;
    let c_user = to_cstring(&request.user_id, "user_id")?;
    let c_tsa = to_cstring(&request.tsa_url, "tsa_url")?;
    let c_roles_csv = to_cstring(&request.roles_csv, "roles_csv")?;

    // C string paths used only by path-based signing variants.
    let c_input_path = match request.signing_implementation {
        SigningImplementation::StandardFromPath | SigningImplementation::CawgIdentityFromPath => {
            let s = request.input_path.to_str().ok_or_else(|| {
                anyhow!("Input path is not valid UTF-8: {}", request.input_path.display())
            })?;
            Some(to_cstring(s, "input_path")?)
        }
        _ => None,
    };
    let c_output_path = match request.signing_implementation {
        SigningImplementation::StandardFromPath | SigningImplementation::CawgIdentityFromPath => {
            let s = output_path.to_str().ok_or_else(|| {
                anyhow!("Output path is not valid UTF-8: {}", output_path.display())
            })?;
            Some(to_cstring(s, "output_path")?)
        }
        _ => None,
    };

    std::env::set_var("C2PA_SIGNING_SERVICE_URL", &request.signing_service_url);
    std::env::set_var("C2PA_API_KEY", &request.api_key);
    std::env::set_var(
        "C2PA_SKIP_SSL_VALIDATION",
        request.skip_ssl_validation.to_string(),
    );
    std::env::set_var(
        "SKIP_C2PA_PUBLIC_TRUST_LIST_CHECK",
        request.skip_c2pa_public_trust_list_check.to_string(),
    );
    std::env::set_var("C2PA_CLAIM_GENERATOR_NAME", "DigiCert Content Trust Manager");
    std::env::set_var("C2PA_CLAIM_GENERATOR_VERSION", "1.0.0");
    if let Some(smime_credential_id) = &request.smime_credential_id {
        std::env::set_var("C2PA_SMIME_CREDENTIAL_ID", smime_credential_id);
    }
    if let Some(user_pin) = &request.user_pin {
        std::env::set_var("C2PA_USER_PIN", user_pin);
    }

    let lib = unsafe {
        Library::new(&request.binary_path).with_context(|| {
            format!(
                "Failed to load dynamic library: {}",
                request.binary_path.display()
            )
        })?
    };

    let free_result: Symbol<C2paFreeResultFn> = unsafe {
        lib.get(b"c2pa_free_result\0")
            .context("Symbol 'c2pa_free_result' not found in the library")?
    };

    let c_training_json = match &request.cawg_training_mining_json {
        Some(value) => Some(to_cstring(value, "cawg_training_mining_json")?),
        None => None,
    };

    let result_ptr = unsafe {
        match request.signing_implementation {
            SigningImplementation::Standard => {
                let sign_content: Symbol<C2paSignContentFn> = lib
                    .get(b"c2pa_sign_content\0")
                    .context("Symbol 'c2pa_sign_content' not found in the library")?;

                sign_content(
                    image_data.as_ref().unwrap().as_ptr(),
                    image_data.as_ref().unwrap().len(),
                    c_filename.as_ptr(),
                    c_roles_csv.as_ptr(),
                    c_tsa.as_ptr(),
                    c_account.as_ptr(),
                    c_user.as_ptr(),
                )
            }
            SigningImplementation::CawgIdentity => {
                let sign_with_cawg_identity: Symbol<C2paSignContentWithCawgIdentityFn> = lib
                    .get(b"c2pa_sign_content_with_cawg_identity\0")
                    .context("Symbol 'c2pa_sign_content_with_cawg_identity' not found in the library")?;

                sign_with_cawg_identity(
                    image_data.as_ref().unwrap().as_ptr(),
                    image_data.as_ref().unwrap().len(),
                    c_filename.as_ptr(),
                    c_roles_csv.as_ptr(),
                    c_training_json
                        .as_ref()
                        .map_or(std::ptr::null(), |s| s.as_ptr()),
                    c_tsa.as_ptr(),
                    c_account.as_ptr(),
                    c_user.as_ptr(),
                )
            }
            SigningImplementation::StandardFromPath => {
                let sign_from_path: Symbol<C2paSignContentFromPathFn> = lib
                    .get(b"c2pa_sign_content_from_path\0")
                    .context("Symbol 'c2pa_sign_content_from_path' not found in the library")?;

                sign_from_path(
                    c_input_path.as_ref().unwrap().as_ptr(),
                    c_output_path.as_ref().unwrap().as_ptr(),
                    c_filename.as_ptr(),
                    c_roles_csv.as_ptr(),
                    c_tsa.as_ptr(),
                    c_account.as_ptr(),
                    c_user.as_ptr(),
                )
            }
            SigningImplementation::CawgIdentityFromPath => {
                let sign_cawg_from_path: Symbol<C2paSignContentWithCawgIdentityFromPathFn> = lib
                    .get(b"c2pa_sign_content_with_cawg_identity_from_path\0")
                    .context("Symbol 'c2pa_sign_content_with_cawg_identity_from_path' not found in the library")?;

                sign_cawg_from_path(
                    c_input_path.as_ref().unwrap().as_ptr(),
                    c_output_path.as_ref().unwrap().as_ptr(),
                    c_filename.as_ptr(),
                    c_roles_csv.as_ptr(),
                    c_training_json
                        .as_ref()
                        .map_or(std::ptr::null(), |s| s.as_ptr()),
                    c_tsa.as_ptr(),
                    c_account.as_ptr(),
                    c_user.as_ptr(),
                )
            }
        }
    };

    if result_ptr.is_null() {
        return Err(anyhow!("Signing function returned a null pointer"));
    }

    let (error_msg, manifest_id, signed_bytes_opt, path_written) = unsafe {
        let result = &*result_ptr;

        let error_msg = if !result.error_message.is_null() {
            Some(CStr::from_ptr(result.error_message).to_string_lossy().into_owned())
        } else {
            None
        };

        let manifest_id = if !result.manifest_id.is_null() {
            Some(
                CStr::from_ptr(result.manifest_id)
                    .to_string_lossy()
                    .into_owned(),
            )
        } else {
            None
        };

        let path_written = !result.output_path.is_null();

        let signed_bytes_opt =
            if !path_written && !result.signed_data.is_null() && result.signed_data_len > 0 {
                Some(
                    std::slice::from_raw_parts(result.signed_data, result.signed_data_len)
                        .to_vec(),
                )
            } else {
                None
            };

        free_result(result_ptr);
        (error_msg, manifest_id, signed_bytes_opt, path_written)
    };

    if let Some(msg) = error_msg {
        return Err(anyhow!("Signing failed: {msg}"));
    }

    // Path-based signing: the library already wrote the output file.
    if path_written {
        return Ok(SignImageResponse {
            output_size: fs::metadata(&output_path).map(|m| m.len() as usize).unwrap_or(0),
            output_path,
            manifest_id,
        });
    }

    let signed_bytes = signed_bytes_opt
        .ok_or_else(|| anyhow!("Signing succeeded but no signed bytes were returned"))?;

    fs::write(&output_path, &signed_bytes)
        .with_context(|| format!("Failed to write signed image to: {}", output_path.display()))?;

    Ok(SignImageResponse {
        output_size: signed_bytes.len(),
        output_path,
        manifest_id,
    })
}

fn file_name(path: &Path) -> Result<&str> {
    path.file_name()
        .and_then(|s| s.to_str())
        .ok_or_else(|| anyhow!("Path does not contain a valid UTF-8 file name: {}", path.display()))
}

fn signed_output_path(
    input_path: &Path,
    output_folder: &Path,
    signing_implementation: SigningImplementation,
) -> Result<PathBuf> {
    let input_name = file_name(input_path)?;
    let input_name_path = Path::new(input_name);
    let stem = input_name_path
        .file_stem()
        .and_then(|value| value.to_str())
        .ok_or_else(|| {
            anyhow!(
                "Input path does not contain a valid UTF-8 file stem: {}",
                input_path.display()
            )
        })?;

    let output_suffix = match signing_implementation {
        SigningImplementation::Standard | SigningImplementation::StandardFromPath => "_signed",
        SigningImplementation::CawgIdentity | SigningImplementation::CawgIdentityFromPath => "_cawg_signed",
    };

    let signed_name = match input_name_path.extension().and_then(|value| value.to_str()) {
        Some(extension) if !extension.is_empty() => format!("{stem}{output_suffix}.{extension}"),
        _ => format!("{stem}{output_suffix}"),
    };

    Ok(output_folder.join(signed_name))
}

fn to_cstring(value: &str, field: &str) -> Result<CString> {
    CString::new(value).with_context(|| format!("Field '{field}' contains an interior NUL byte"))
}

fn sanitize_request(request: &mut SignImageRequest) -> Result<()> {
    request.account_id = sanitize_required_field(&request.account_id, "account_id", 256)?;
    request.user_id = if request.user_id.trim().is_empty() {
        String::new()
    } else {
        sanitize_optional_field(&request.user_id, "user_id", 256)?
    };
    request.api_key = sanitize_secret(&request.api_key, "api_key", 4096)?;
    request.signing_service_url =
        sanitize_required_field(&request.signing_service_url, "signing_service_url", 2048)?;
    request.tsa_url = sanitize_required_field(&request.tsa_url, "tsa_url", 2048)?;
    request.roles_csv = sanitize_required_field(&request.roles_csv, "roles_csv", 1024)?;

    if let Some(value) = &request.cawg_training_mining_json {
        request.cawg_training_mining_json = Some(sanitize_optional_field(
            value,
            "cawg_training_mining_json",
            64 * 1024,
        )?);
    }

    if let Some(value) = &request.smime_credential_id {
        request.smime_credential_id = Some(sanitize_optional_field(value, "smime_credential_id", 256)?);
    }

    if matches!(
        request.signing_implementation,
        SigningImplementation::CawgIdentity | SigningImplementation::CawgIdentityFromPath
    ) && request.smime_credential_id.is_none()
    {
        return Err(anyhow!(
            "Field 'smime_credential_id' is required for CAWG identity signing"
        ));
    }

    if let Some(value) = &request.user_pin {
        request.user_pin = Some(sanitize_secret(value, "user_pin", 128)?);
    }

    if matches!(
        request.signing_implementation,
        SigningImplementation::CawgIdentity | SigningImplementation::CawgIdentityFromPath
    ) && request.user_pin.is_none()
    {
        return Err(anyhow!(
            "Field 'user_pin' is required for CAWG identity signing"
        ));
    }

    Ok(())
}

fn sanitize_required_field(value: &str, field: &str, max_len: usize) -> Result<String> {
    let trimmed = value.trim();

    if trimmed.is_empty() {
        return Err(anyhow!("Field '{field}' must not be empty"));
    }

    if trimmed.len() > max_len {
        return Err(anyhow!("Field '{field}' exceeds max length of {max_len}"));
    }

    if trimmed.chars().any(|ch| ch.is_control()) {
        return Err(anyhow!(
            "Field '{field}' contains control characters and is not allowed"
        ));
    }

    Ok(trimmed.to_string())
}

fn sanitize_optional_field(value: &str, field: &str, max_len: usize) -> Result<String> {
    let trimmed = value.trim();

    if trimmed.is_empty() {
        return Err(anyhow!("Optional field '{field}' was provided but is empty"));
    }

    sanitize_required_field(trimmed, field, max_len)
}

fn sanitize_secret(value: &str, field: &str, max_len: usize) -> Result<String> {
    let sanitized = sanitize_required_field(value, field, max_len)?;
    let lowered = sanitized.to_ascii_lowercase();

    let looks_placeholder = [
        "default",
        "changeme",
        "change-me",
        "your_api_key",
        "your-api-key",
        "api_key",
        "password",
    ]
    .iter()
    .any(|token| lowered == *token || lowered.contains(token));

    if looks_placeholder {
        return Err(anyhow!(
            "Field '{field}' appears to contain a placeholder secret; pass a real value via environment variables or a secret manager"
        ));
    }

    Ok(sanitized)
}


