//! Secret-safe clipboard lifecycle and platform-neutral QR presentation.

use std::{error::Error, fmt, time::Duration};

use qrcode::{EcLevel, QrCode, types::Color};
use zeroize::Zeroize as _;

use crate::repository::SecretBytes;

mod platform;

pub const DEFAULT_CLIPBOARD_TIMEOUT: Duration = Duration::from_secs(45);
pub const MAX_CLIPBOARD_TIMEOUT: Duration = Duration::from_secs(5 * 60);
const QR_QUIET_ZONE: usize = 4;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ClipboardTimeout(Duration);

impl ClipboardTimeout {
    pub fn new(duration: Duration) -> Result<Self, ClipboardError> {
        if duration.is_zero() || duration > MAX_CLIPBOARD_TIMEOUT {
            return Err(ClipboardError::InvalidTimeout);
        }
        Ok(Self(duration))
    }

    pub const fn pass_default() -> Self {
        Self(DEFAULT_CLIPBOARD_TIMEOUT)
    }

    pub const fn duration(self) -> Duration {
        self.0
    }
}

impl Default for ClipboardTimeout {
    fn default() -> Self {
        Self::pass_default()
    }
}

pub enum ClipboardContent {
    Text(SecretBytes),
    EmptyOrNonText,
}

impl ClipboardContent {
    pub fn text(value: Vec<u8>) -> Self {
        Self::Text(SecretBytes::new(value))
    }
}

impl fmt::Debug for ClipboardContent {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Text(_) => formatter.write_str("ClipboardContent::Text([REDACTED])"),
            Self::EmptyOrNonText => formatter.write_str("ClipboardContent::EmptyOrNonText"),
        }
    }
}

/// Byte-oriented adapter contract for native clipboards and deterministic tests.
pub trait ClipboardBackend {
    fn read(&mut self) -> Result<ClipboardContent, ClipboardError>;
    fn write(&mut self, value: &SecretBytes) -> Result<(), ClipboardError>;
    fn clear(&mut self) -> Result<(), ClipboardError>;
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ClipboardWait {
    Elapsed,
    Cancelled,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ClipboardDisposition {
    RestoredPrevious,
    Cleared,
    PreservedNewer,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ClipboardError {
    InvalidTimeout,
    EmptySecret,
    NonUtf8,
    Unavailable,
    ReadFailed,
    WriteFailed,
    CleanupFailed,
    Cancelled,
}

impl fmt::Display for ClipboardError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let message = match self {
            Self::InvalidTimeout => "the clipboard timeout is invalid",
            Self::EmptySecret => "empty data cannot be presented on the clipboard",
            Self::NonUtf8 => "the native clipboard accepts only UTF-8 text",
            Self::Unavailable => "the native clipboard is unavailable",
            Self::ReadFailed => "the native clipboard could not be read",
            Self::WriteFailed => "the native clipboard could not be written",
            Self::CleanupFailed => "the clipboard secret could not be cleaned up safely",
            Self::Cancelled => "clipboard presentation was cancelled",
        };
        formatter.write_str(message)
    }
}

impl Error for ClipboardError {}

pub struct ClipboardManager<B> {
    backend: B,
    timeout: ClipboardTimeout,
}

impl<B> fmt::Debug for ClipboardManager<B> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("ClipboardManager")
            .field("timeout", &self.timeout)
            .field("contents", &"[REDACTED]")
            .finish()
    }
}

impl<B: ClipboardBackend> ClipboardManager<B> {
    pub fn new(backend: B, timeout: ClipboardTimeout) -> Self {
        Self { backend, timeout }
    }

    pub fn timeout(&self) -> ClipboardTimeout {
        self.timeout
    }

    /// Copy a secret, wait under caller-controlled scheduling, and then clean up.
    ///
    /// Cleanup restores the previous UTF-8 value (or clears a previous non-text
    /// value) only while the copied secret is still current. A newer clipboard
    /// value is never overwritten.
    pub fn copy_with(
        &mut self,
        value: &SecretBytes,
        wait: impl FnOnce(Duration) -> ClipboardWait,
    ) -> Result<ClipboardDisposition, ClipboardError> {
        if value.expose().is_empty() {
            return Err(ClipboardError::EmptySecret);
        }
        let previous = self.backend.read()?;
        self.backend.write(value)?;
        let wait_result = wait(self.timeout.duration());
        let disposition = self.cleanup(value, &previous)?;
        if wait_result == ClipboardWait::Cancelled {
            return Err(ClipboardError::Cancelled);
        }
        Ok(disposition)
    }

    fn cleanup(
        &mut self,
        copied: &SecretBytes,
        previous: &ClipboardContent,
    ) -> Result<ClipboardDisposition, ClipboardError> {
        let current = self
            .backend
            .read()
            .map_err(|_| ClipboardError::CleanupFailed)?;
        let ClipboardContent::Text(current) = current else {
            return Ok(ClipboardDisposition::PreservedNewer);
        };
        if current.expose() != copied.expose() {
            return Ok(ClipboardDisposition::PreservedNewer);
        }
        match previous {
            ClipboardContent::Text(previous) => self
                .backend
                .write(previous)
                .map(|()| ClipboardDisposition::RestoredPrevious)
                .map_err(|_| ClipboardError::CleanupFailed),
            ClipboardContent::EmptyOrNonText => self
                .backend
                .clear()
                .map(|()| ClipboardDisposition::Cleared)
                .map_err(|_| ClipboardError::CleanupFailed),
        }
    }
}

pub type NativeClipboardManager = ClipboardManager<platform::NativeClipboardBackend>;

impl NativeClipboardManager {
    pub fn system(timeout: ClipboardTimeout) -> Result<Self, ClipboardError> {
        Ok(Self::new(platform::NativeClipboardBackend::new()?, timeout))
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QrError {
    EmptyPayload,
    PayloadTooLarge,
    InvalidImage,
    NotFound,
    InvalidPayload,
}

impl fmt::Display for QrError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyPayload => formatter.write_str("empty data cannot be encoded as a QR code"),
            Self::PayloadTooLarge => formatter.write_str("the QR payload is too large"),
            Self::InvalidImage => formatter.write_str("the selected file is not a supported image"),
            Self::NotFound => formatter.write_str("the image does not contain a QR code"),
            Self::InvalidPayload => formatter.write_str("the QR code contains invalid text"),
        }
    }
}

impl Error for QrError {}

/// A secret-derived, zeroizing QR symbol without presentation-platform choices.
pub struct QrMatrix {
    width: usize,
    modules: Vec<u8>,
}

impl QrMatrix {
    pub fn encode(payload: &SecretBytes) -> Result<Self, QrError> {
        if payload.expose().is_empty() {
            return Err(QrError::EmptyPayload);
        }
        let code = QrCode::with_error_correction_level(payload.expose(), EcLevel::L)
            .map_err(|_| QrError::PayloadTooLarge)?;
        let width = code.width();
        let modules = code
            .into_colors()
            .into_iter()
            .map(|color| u8::from(color == Color::Dark))
            .collect();
        Ok(Self { width, modules })
    }

    /// Decode the first QR symbol from an encoded image without exposing the
    /// secret-derived payload to a presentation adapter.
    pub fn decode_image(image: &SecretBytes) -> Result<SecretBytes, QrError> {
        let grayscale = image::load_from_memory(image.expose())
            .map_err(|_| QrError::InvalidImage)?
            .into_luma8();
        let mut prepared = rqrr::PreparedImage::prepare_from_greyscale(
            grayscale.width() as usize,
            grayscale.height() as usize,
            |x, y| grayscale.get_pixel(x as u32, y as u32).0[0],
        );
        let grid = prepared
            .detect_grids()
            .into_iter()
            .next()
            .ok_or(QrError::NotFound)?;
        let (_, payload) = grid.decode().map_err(|_| QrError::InvalidPayload)?;
        Ok(SecretBytes::new(payload.into_bytes()))
    }

    pub fn width(&self) -> usize {
        self.width
    }

    pub fn is_dark(&self, x: usize, y: usize) -> Option<bool> {
        if x >= self.width || y >= self.width {
            return None;
        }
        Some(self.modules[y * self.width + x] != 0)
    }

    /// Render a QR symbol with the standard four-module quiet zone and square
    /// terminal cells. The returned bytes are secret-derived and zeroize on drop.
    pub fn render_terminal(&self) -> SecretBytes {
        let padded_width = self.width + 2 * QR_QUIET_ZONE;
        let padded_height = padded_width.next_multiple_of(2);
        let mut rendered = String::with_capacity(padded_width * padded_height * 2);
        for y in (0..padded_height).step_by(2) {
            for x in 0..padded_width {
                let top = self.padded_module(x, y);
                let bottom = self.padded_module(x, y + 1);
                let cell = match (top, bottom) {
                    (true, true) => '█',
                    (true, false) => '▀',
                    (false, true) => '▄',
                    (false, false) => ' ',
                };
                rendered.push(cell);
                rendered.push(cell);
            }
            rendered.push('\n');
        }
        SecretBytes::new(rendered.into_bytes())
    }

    fn padded_module(&self, x: usize, y: usize) -> bool {
        let Some(x) = x.checked_sub(QR_QUIET_ZONE) else {
            return false;
        };
        let Some(y) = y.checked_sub(QR_QUIET_ZONE) else {
            return false;
        };
        self.is_dark(x, y).unwrap_or(false)
    }
}

impl Clone for QrMatrix {
    fn clone(&self) -> Self {
        Self {
            width: self.width,
            modules: self.modules.clone(),
        }
    }
}

impl fmt::Debug for QrMatrix {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("QrMatrix([REDACTED])")
    }
}

impl Drop for QrMatrix {
    fn drop(&mut self) {
        self.modules.zeroize();
    }
}
