//! WASM Plugin Interface Types for rustledger
//!
//! This crate provides the type definitions for rustledger's WASM plugin interface.
//! Use it as a dependency in your plugin crate to ensure type compatibility with
//! the rustledger host.
//!
//! # Quick Start
//!
//! Add this to your plugin's `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! rustledger-plugin-types = "0.10"
//! rmp-serde = "1"
//! ```
//!
//! Then in your plugin:
//!
//! ```rust,ignore
//! use rustledger_plugin_types::*;
//!
//! #[no_mangle]
//! pub extern "C" fn process(input_ptr: u32, input_len: u32) -> u64 {
//!     let input_bytes = unsafe {
//!         std::slice::from_raw_parts(input_ptr as *const u8, input_len as usize)
//!     };
//!
//!     let input: PluginInput = rmp_serde::from_slice(input_bytes).unwrap();
//!
//!     // Process directives...
//!     let output = PluginOutput {
//!         directives: input.directives,
//!         errors: vec![],
//!     };
//!
//!     let output_bytes = rmp_serde::to_vec(&output).unwrap();
//!     let output_ptr = alloc(output_bytes.len() as u32);
//!     unsafe {
//!         std::ptr::copy_nonoverlapping(
//!             output_bytes.as_ptr(),
//!             output_ptr,
//!             output_bytes.len(),
//!         );
//!     }
//!     ((output_ptr as u64) << 32) | (output_bytes.len() as u64)
//! }
//!
//! #[no_mangle]
//! pub extern "C" fn alloc(size: u32) -> *mut u8 {
//!     let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
//!     unsafe { std::alloc::alloc(layout) }
//! }
//! ```
//!
//! # Serialization Format
//!
//! Plugins communicate with the host via `MessagePack` serialization. The host
//! calls `process(ptr, len)` with a pointer to MessagePack-encoded [`PluginInput`].
//! The plugin returns a packed u64 containing a pointer and length to
//! MessagePack-encoded [`PluginOutput`].
//!
//! # Memory Management
//!
//! Plugins must export an `alloc(size: u32) -> *mut u8` function. The host uses
//! this to allocate memory in the WASM linear memory for passing input data.
//! The plugin uses it to allocate memory for output data.
//!
//! Optionally, plugins can export a `dealloc(ptr: *mut u8, size: u32)` function
//! to free memory. This is not required by the host but can be useful for
//! memory management within longer-running plugin operations.
//!
//! # Version Compatibility
//!
//! Plugin types are versioned with rustledger. For best compatibility, use the
//! same minor version of `rustledger-plugin-types` as the rustledger host you're
//! targeting (e.g., `0.10.x` for rustledger `0.10.x`).
//!
//! # Building
//!
//! Build your plugin for the WASM target:
//!
//! ```sh
//! rustup target add wasm32-unknown-unknown
//! cargo build --target wasm32-unknown-unknown --release
//! ```
//!
//! The output will be in `target/wasm32-unknown-unknown/release/your_plugin.wasm`

use serde::{Deserialize, Serialize};

// ============================================================================
// Top-Level Plugin Interface
// ============================================================================

/// Input passed to a plugin.
///
/// The host serializes this struct via `MessagePack` and passes it to the
/// plugin's `process` function.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInput {
    /// All directives to process.
    pub directives: Vec<DirectiveWrapper>,
    /// Ledger options.
    pub options: PluginOptions,
    /// Plugin-specific configuration string (from the plugin directive).
    ///
    /// For example, `plugin "myplugin.wasm" "threshold=100"` would set
    /// `config` to `Some("threshold=100")`.
    pub config: Option<String>,
}

/// Output returned from a plugin.
///
/// The plugin serializes this struct via `MessagePack` and returns a pointer
/// to it from the `process` function.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginOutput {
    /// Processed directives (may be modified, added, or removed).
    pub directives: Vec<DirectiveWrapper>,
    /// Errors generated by the plugin.
    pub errors: Vec<PluginError>,
}

impl PluginOutput {
    /// Create an output that passes through directives unchanged.
    #[must_use]
    pub const fn passthrough(directives: Vec<DirectiveWrapper>) -> Self {
        Self {
            directives,
            errors: Vec::new(),
        }
    }
}

/// Ledger options passed to plugins.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginOptions {
    /// Operating currencies (from `option "operating_currency" "USD"`).
    pub operating_currencies: Vec<String>,
    /// Ledger title (from `option "title" "My Ledger"`).
    pub title: Option<String>,
}

// ============================================================================
// Plugin Errors
// ============================================================================

/// Error generated by a plugin.
///
/// Use [`PluginError::error`] or [`PluginError::warning`] to create errors,
/// and optionally chain [`PluginError::at`] to set the source location.
///
/// # Example
///
/// ```
/// use rustledger_plugin_types::{PluginError, PluginErrorSeverity};
///
/// let error = PluginError::error("Invalid transaction")
///     .at("ledger.beancount", 42);
///
/// let warning = PluginError::warning("Duplicate entry detected");
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginError {
    /// Error message.
    pub message: String,
    /// Source file (if known).
    pub source_file: Option<String>,
    /// Line number (if known).
    pub line_number: Option<u32>,
    /// Error severity.
    pub severity: PluginErrorSeverity,
}

/// Severity of a plugin error.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PluginErrorSeverity {
    /// Warning - processing continues.
    #[serde(rename = "warning")]
    Warning,
    /// Error - ledger is marked invalid.
    #[serde(rename = "error")]
    Error,
}

impl PluginError {
    /// Create a new error.
    #[must_use]
    pub fn error(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
            source_file: None,
            line_number: None,
            severity: PluginErrorSeverity::Error,
        }
    }

    /// Create a new warning.
    #[must_use]
    pub fn warning(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
            source_file: None,
            line_number: None,
            severity: PluginErrorSeverity::Warning,
        }
    }

    /// Set the source location.
    #[must_use]
    pub fn at(mut self, file: impl Into<String>, line: u32) -> Self {
        self.source_file = Some(file.into());
        self.line_number = Some(line);
        self
    }
}

// ============================================================================
// Directive Types
// ============================================================================

/// A wrapper around directives for serialization.
///
/// This wrapper provides a uniform interface for all directive types,
/// with source location tracking for error reporting.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectiveWrapper {
    /// The type of directive (derived from data, not serialized to avoid duplicate keys).
    #[serde(skip_serializing, default)]
    pub directive_type: String,
    /// The directive date (YYYY-MM-DD format).
    pub date: String,
    /// Source filename (for tracking through plugin processing).
    /// If None, the directive was created by a plugin.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub filename: Option<String>,
    /// Source line number (1-based).
    /// If None, the directive was created by a plugin.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub lineno: Option<u32>,
    /// Directive-specific data as a nested structure.
    #[serde(flatten)]
    pub data: DirectiveData,
}

impl DirectiveWrapper {
    /// Returns the sort order for directive types, matching Python beancount's `SORT_ORDER`.
    ///
    /// Order ensures logical processing:
    /// - Open (-2): Accounts must be opened first
    /// - Balance (-1): Balance assertions checked before transactions
    /// - Default (0): Transactions, Commodity, Pad, Event, Note, Price, Query, Custom
    /// - Document (1): Documents recorded after transactions
    /// - Close (2): Accounts closed last
    #[must_use]
    pub const fn type_sort_order(&self) -> i8 {
        match &self.data {
            DirectiveData::Open(_) => -2,
            DirectiveData::Balance(_) => -1,
            DirectiveData::Document(_) => 1,
            DirectiveData::Close(_) => 2,
            _ => 0,
        }
    }

    /// Returns a sort key tuple matching Python beancount's `entry_sortkey()`.
    ///
    /// Sorts by: (date, `type_order`, lineno)
    #[must_use]
    pub fn sort_key(&self) -> (&str, i8, u32) {
        (
            &self.date,
            self.type_sort_order(),
            self.lineno.unwrap_or(u32::MAX),
        )
    }
}

/// Directive-specific data.
///
/// Each variant corresponds to a Beancount directive type.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum DirectiveData {
    /// Transaction data.
    #[serde(rename = "transaction")]
    Transaction(TransactionData),
    /// Balance assertion data.
    #[serde(rename = "balance")]
    Balance(BalanceData),
    /// Open account data.
    #[serde(rename = "open")]
    Open(OpenData),
    /// Close account data.
    #[serde(rename = "close")]
    Close(CloseData),
    /// Commodity declaration data.
    #[serde(rename = "commodity")]
    Commodity(CommodityData),
    /// Pad directive data.
    #[serde(rename = "pad")]
    Pad(PadData),
    /// Event data.
    #[serde(rename = "event")]
    Event(EventData),
    /// Note data.
    #[serde(rename = "note")]
    Note(NoteData),
    /// Document data.
    #[serde(rename = "document")]
    Document(DocumentData),
    /// Price data.
    #[serde(rename = "price")]
    Price(PriceData),
    /// Query data.
    #[serde(rename = "query")]
    Query(QueryData),
    /// Custom directive data.
    #[serde(rename = "custom")]
    Custom(CustomData),
}

// ============================================================================
// Transaction Types
// ============================================================================

/// Transaction data for serialization.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionData {
    /// Transaction flag (`*` for complete, `!` for incomplete/pending).
    pub flag: String,
    /// Optional payee.
    pub payee: Option<String>,
    /// Narration/description.
    pub narration: String,
    /// Tags without the `#` prefix.
    pub tags: Vec<String>,
    /// Links without the `^` prefix.
    pub links: Vec<String>,
    /// Metadata key-value pairs.
    pub metadata: Vec<(String, MetaValueData)>,
    /// Postings.
    pub postings: Vec<PostingData>,
}

/// Posting data for serialization.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostingData {
    /// Account name (e.g., `Assets:Bank:Checking`).
    pub account: String,
    /// Units (amount + currency). None for auto-balanced postings.
    pub units: Option<AmountData>,
    /// Cost specification (for lot tracking).
    pub cost: Option<CostData>,
    /// Price annotation (@ or @@).
    pub price: Option<PriceAnnotationData>,
    /// Optional posting flag.
    pub flag: Option<String>,
    /// Posting metadata.
    pub metadata: Vec<(String, MetaValueData)>,
}

/// Amount data for serialization.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AmountData {
    /// Number as string (preserves precision).
    pub number: String,
    /// Currency code.
    pub currency: String,
}

/// Cost data for serialization.
///
/// Represents cost specifications like `{100 USD}` or `{100 USD, 2024-01-01, "lot1"}`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostData {
    /// Per-unit cost number.
    pub number_per: Option<String>,
    /// Total cost number.
    pub number_total: Option<String>,
    /// Cost currency.
    pub currency: Option<String>,
    /// Acquisition date.
    pub date: Option<String>,
    /// Lot label.
    pub label: Option<String>,
    /// Merge lots flag.
    pub merge: bool,
}

/// Price annotation data.
///
/// Represents price annotations like `@ 100 USD` or `@@ 1000 USD` (total price).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceAnnotationData {
    /// Whether this is a total price (`@@`) vs per-unit (`@`).
    pub is_total: bool,
    /// The price amount (optional for incomplete/empty prices).
    pub amount: Option<AmountData>,
    /// The number only (for incomplete prices).
    pub number: Option<String>,
    /// The currency only (for incomplete prices).
    pub currency: Option<String>,
}

// ============================================================================
// Metadata Types
// ============================================================================

/// Metadata value for serialization.
///
/// Metadata can hold various types of values, preserving type information
/// for accurate round-tripping.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum MetaValueData {
    /// String value.
    #[serde(rename = "string")]
    String(String),
    /// Number value (as string to preserve precision).
    #[serde(rename = "number")]
    Number(String),
    /// Date value (YYYY-MM-DD).
    #[serde(rename = "date")]
    Date(String),
    /// Account reference.
    #[serde(rename = "account")]
    Account(String),
    /// Currency reference.
    #[serde(rename = "currency")]
    Currency(String),
    /// Tag reference.
    #[serde(rename = "tag")]
    Tag(String),
    /// Link reference.
    #[serde(rename = "link")]
    Link(String),
    /// Amount value.
    #[serde(rename = "amount")]
    Amount(AmountData),
    /// Boolean value.
    #[serde(rename = "bool")]
    Bool(bool),
}

// ============================================================================
// Other Directive Types
// ============================================================================

/// Balance assertion data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BalanceData {
    /// Account name.
    pub account: String,
    /// Expected balance.
    pub amount: AmountData,
    /// Tolerance for balance check.
    pub tolerance: Option<String>,
    /// Metadata key-value pairs.
    #[serde(default)]
    pub metadata: Vec<(String, MetaValueData)>,
}

/// Open account data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenData {
    /// Account name.
    pub account: String,
    /// Allowed currencies (empty means any currency).
    pub currencies: Vec<String>,
    /// Booking method (FIFO, LIFO, etc.).
    pub booking: Option<String>,
    /// Metadata key-value pairs.
    #[serde(default)]
    pub metadata: Vec<(String, MetaValueData)>,
}

/// Close account data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloseData {
    /// Account name.
    pub account: String,
    /// Metadata key-value pairs.
    #[serde(default)]
    pub metadata: Vec<(String, MetaValueData)>,
}

/// Commodity declaration data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommodityData {
    /// Currency code.
    pub currency: String,
    /// Metadata key-value pairs.
    #[serde(default)]
    pub metadata: Vec<(String, MetaValueData)>,
}

/// Pad directive data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PadData {
    /// Account to pad.
    pub account: String,
    /// Source account for padding.
    pub source_account: String,
    /// Metadata key-value pairs.
    #[serde(default)]
    pub metadata: Vec<(String, MetaValueData)>,
}

/// Event data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventData {
    /// Event type.
    pub event_type: String,
    /// Event value.
    pub value: String,
    /// Metadata key-value pairs.
    #[serde(default)]
    pub metadata: Vec<(String, MetaValueData)>,
}

/// Note data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoteData {
    /// Account name.
    pub account: String,
    /// Note comment.
    pub comment: String,
    /// Metadata key-value pairs.
    #[serde(default)]
    pub metadata: Vec<(String, MetaValueData)>,
}

/// Document data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentData {
    /// Account name.
    pub account: String,
    /// Document path.
    pub path: String,
    /// Metadata key-value pairs.
    #[serde(default)]
    pub metadata: Vec<(String, MetaValueData)>,
}

/// Price directive data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceData {
    /// Currency being priced.
    pub currency: String,
    /// Price amount.
    pub amount: AmountData,
    /// Metadata key-value pairs.
    #[serde(default)]
    pub metadata: Vec<(String, MetaValueData)>,
}

/// Query directive data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryData {
    /// Query name.
    pub name: String,
    /// Query string (BQL).
    pub query: String,
    /// Metadata key-value pairs.
    #[serde(default)]
    pub metadata: Vec<(String, MetaValueData)>,
}

/// Custom directive data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomData {
    /// Custom type (first value after `custom` keyword).
    pub custom_type: String,
    /// Values preserving their types.
    pub values: Vec<MetaValueData>,
    /// Metadata key-value pairs.
    #[serde(default)]
    pub metadata: Vec<(String, MetaValueData)>,
}

// ============================================================================
// Utility Functions
// ============================================================================

/// Sort directives using beancount's standard ordering.
///
/// This matches Python beancount's `entry_sortkey()`:
/// 1. Primary: date
/// 2. Secondary: directive type (Open, Balance, default, Document, Close)
/// 3. Tertiary: line number (preserves file order for same-date, same-type entries)
pub fn sort_directives(directives: &mut [DirectiveWrapper]) {
    directives.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_plugin_error_builder() {
        let error = PluginError::error("test error").at("file.beancount", 10);
        assert_eq!(error.message, "test error");
        assert_eq!(error.source_file, Some("file.beancount".to_string()));
        assert_eq!(error.line_number, Some(10));
        assert_eq!(error.severity, PluginErrorSeverity::Error);
    }

    #[test]
    fn test_plugin_warning() {
        let warning = PluginError::warning("test warning");
        assert_eq!(warning.severity, PluginErrorSeverity::Warning);
    }

    #[test]
    fn test_directive_sort_order() {
        let open = DirectiveWrapper {
            directive_type: String::new(),
            date: "2024-01-01".to_string(),
            filename: None,
            lineno: Some(1),
            data: DirectiveData::Open(OpenData {
                account: "Assets:Bank".to_string(),
                currencies: vec![],
                booking: None,
                metadata: vec![],
            }),
        };
        assert_eq!(open.type_sort_order(), -2);

        let close = DirectiveWrapper {
            directive_type: String::new(),
            date: "2024-01-01".to_string(),
            filename: None,
            lineno: Some(2),
            data: DirectiveData::Close(CloseData {
                account: "Assets:Bank".to_string(),
                metadata: vec![],
            }),
        };
        assert_eq!(close.type_sort_order(), 2);
    }

    #[test]
    fn test_serde_roundtrip() {
        let input = PluginInput {
            directives: vec![DirectiveWrapper {
                directive_type: String::new(),
                date: "2024-01-15".to_string(),
                filename: Some("test.beancount".to_string()),
                lineno: Some(42),
                data: DirectiveData::Transaction(TransactionData {
                    flag: "*".to_string(),
                    payee: Some("Coffee Shop".to_string()),
                    narration: "Morning coffee".to_string(),
                    tags: vec!["food".to_string()],
                    links: vec![],
                    metadata: vec![],
                    postings: vec![PostingData {
                        account: "Expenses:Food".to_string(),
                        units: Some(AmountData {
                            number: "5.00".to_string(),
                            currency: "USD".to_string(),
                        }),
                        cost: None,
                        price: None,
                        flag: None,
                        metadata: vec![],
                    }],
                }),
            }],
            options: PluginOptions {
                operating_currencies: vec!["USD".to_string()],
                title: Some("Test Ledger".to_string()),
            },
            config: Some("threshold=100".to_string()),
        };

        // Test JSON roundtrip
        let json = serde_json::to_string(&input).unwrap();
        let decoded: PluginInput = serde_json::from_str(&json).unwrap();
        assert_eq!(decoded.directives.len(), 1);
        assert_eq!(decoded.config, Some("threshold=100".to_string()));

        // Test MessagePack roundtrip
        let msgpack = rmp_serde::to_vec(&input).unwrap();
        let decoded: PluginInput = rmp_serde::from_slice(&msgpack).unwrap();
        assert_eq!(decoded.directives.len(), 1);
    }
}
