//! Invoice generation: GST computation, line items, invoice number
//! generation, and an HTML renderer that doubles as the printable
//! invoice and the source for the PDF (the frontend or a headless
//! renderer can turn the HTML into a PDF).
//!
//! Money flow:
//!   * `subtotal`  — sum of line subtotals, in paise (integer).
//!   * `discount_amount` — coupon / promo / manual discount, in paise.
//!   * Taxable value = subtotal − discount_amount.
//!   * `cgst_amount` + `sgst_amount` = GST for an intra-state sale.
//!   * `igst_amount` = GST for an inter-state sale.
//!   * `total` = taxable value + GST.
//!
//! We never compute taxes on the discount unless the caller passes a
//! pre-discount taxable value; by default the discount reduces the
//! taxable base first, which is the standard approach.

pub mod html;
pub mod service;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

// ──────────────────────────────────────────────────────────────────────────
// Tax
// ──────────────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaxType {
    /// No tax (inter-state supply to an unregistered person — usually not
    /// applicable for B2C services, but supported for completeness).
    Exempt,
    /// Indian GST — split into CGST + SGST for intra-state, IGST for
    /// inter-state.
    Gst,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GstBreakdown {
    pub taxable_value_paise: i64,
    pub cgst_rate: f64,
    pub cgst_amount: i64,
    pub sgst_rate: f64,
    pub sgst_amount: i64,
    pub igst_rate: f64,
    pub igst_amount: i64,
    pub total_tax_paise: i64,
}

impl GstBreakdown {
    /// Compute a GST split for a given taxable value, total rate
    /// (e.g. 18.0), and whether the supply is intra- or inter-state.
    pub fn compute(
        taxable_value_paise: i64,
        total_rate_percent: f64,
        inter_state: bool,
    ) -> Self {
        if total_rate_percent <= 0.0 || taxable_value_paise <= 0 {
            return Self {
                taxable_value_paise,
                cgst_rate: 0.0,
                cgst_amount: 0,
                sgst_rate: 0.0,
                sgst_amount: 0,
                igst_rate: 0.0,
                igst_amount: 0,
                total_tax_paise: 0,
            };
        }

        // i64 math in paise; round to nearest paise.
        let hundredths = (total_rate_percent * 100.0).round() as i64; // basis points * 100
        let total_cents = taxable_value_paise * hundredths;
        let total_tax = bank_round_div(total_cents, 10_000);

        if inter_state {
            Self {
                taxable_value_paise,
                cgst_rate: 0.0,
                cgst_amount: 0,
                sgst_rate: 0.0,
                sgst_amount: 0,
                igst_rate: total_rate_percent,
                igst_amount: total_tax,
                total_tax_paise: total_tax,
            }
        } else {
            // Split equally between CGST and SGST.
            let half = total_tax / 2;
            let other_half = total_tax - half;
            Self {
                taxable_value_paise,
                cgst_rate: total_rate_percent / 2.0,
                cgst_amount: half,
                sgst_rate: total_rate_percent / 2.0,
                sgst_amount: other_half,
                igst_rate: 0.0,
                igst_amount: 0,
                total_tax_paise: total_tax,
            }
        }
    }
}

/// Banker's rounding (round half to even).
fn bank_round_div(numerator: i64, denominator: i64) -> i64 {
    let q = numerator / denominator;
    let r = numerator % denominator;
    let double_r = r.abs() * 2;
    if double_r < denominator {
        q
    } else if double_r > denominator {
        q + r.signum()
    } else {
        // Exact half — round to even.
        if q % 2 == 0 {
            q
        } else {
            q + r.signum()
        }
    }
}

// ──────────────────────────────────────────────────────────────────────────
// Line items
// ──────────────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineItem {
    pub line_number: i32,
    pub description: String,
    pub hsn_sac_code: Option<String>,
    pub quantity: f64,
    pub unit_price_paise: i64,
    pub tax_rate_percent: f64,
    pub metadata: Option<serde_json::Value>,
}

impl LineItem {
    pub fn subtotal_paise(&self) -> i64 {
        let qty_times = (self.quantity * 1_000_000.0).round() as i64;
        bank_round_div(qty_times * self.unit_price_paise, 1_000_000)
    }

    pub fn tax_paise(&self) -> i64 {
        let sub = self.subtotal_paise();
        GstBreakdown::compute(sub, self.tax_rate_percent, false)
            .total_tax_paise
    }

    pub fn total_paise(&self) -> i64 {
        self.subtotal_paise() + self.tax_paise()
    }
}

// ──────────────────────────────────────────────────────────────────────────
// Invoice number
// ──────────────────────────────────────────────────────────────────────────

/// Format a sequence number into a human-readable invoice number.
///   * year (`YY`)
///   * state code from place-of-supply (e.g. `MH` for Maharashtra)
///   * 6-digit sequence
/// Example: `NXT-INV-26-MH-100001`
pub fn format_invoice_number(
    year: i32,
    place_of_supply_state: Option<&str>,
    seq: i64,
) -> String {
    let yy = year % 100;
    let state = place_of_supply_state.unwrap_or("XX");
    format!("NXT-INV-{:02}-{}-{:06}", yy, state, seq)
}

// ──────────────────────────────────────────────────────────────────────────
// Invoice (read model + write model)
// ──────────────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invoice {
    pub id: Uuid,
    pub invoice_number: String,
    pub payment_id: Uuid,
    pub user_id: Uuid,
    pub status: String,
    pub currency: String,
    pub invoice_type: String,
    pub subtotal: i64,
    pub discount_amount: i64,
    pub cgst_rate: f64,
    pub cgst_amount: i64,
    pub sgst_rate: f64,
    pub sgst_amount: i64,
    pub igst_rate: f64,
    pub igst_amount: i64,
    pub total: i64,
    pub reverse_charge: bool,
    pub seller_name: String,
    pub seller_address: String,
    pub seller_gstin: Option<String>,
    pub seller_pan: Option<String>,
    pub seller_state_code: Option<String>,
    pub place_of_supply_state: Option<String>,
    pub customer_name: Option<String>,
    pub customer_email: Option<String>,
    pub customer_phone: Option<String>,
    pub customer_billing_address: Option<String>,
    pub customer_gstin: Option<String>,
    pub customer_state_code: Option<String>,
    pub discount_label: Option<String>,
    pub notes: Option<String>,
    pub pdf_object_key: Option<String>,
    pub issued_at: DateTime<Utc>,
    pub paid_at: Option<DateTime<Utc>>,
    pub voided_at: Option<DateTime<Utc>>,
    pub voided_by_user_id: Option<Uuid>,
    pub void_reason: Option<String>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BillingDetails {
    pub legal_name: String,
    pub email: Option<String>,
    pub phone: Option<String>,
    pub gstin: Option<String>,
    pub pan: Option<String>,
    pub billing_address: String,
    pub state_code: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SellerDetails {
    pub name: String,
    pub address: String,
    pub gstin: Option<String>,
    pub pan: Option<String>,
    pub state_code: String,
}

/// Totals a single invoice without a database round-trip.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvoiceTotals {
    pub subtotal: i64,
    pub discount: i64,
    pub taxable_value: i64,
    pub cgst: i64,
    pub sgst: i64,
    pub igst: i64,
    pub total_tax: i64,
    pub total: i64,
}

pub fn compute_totals(
    lines: &[LineItem],
    discount: i64,
    inter_state: bool,
) -> InvoiceTotals {
    let subtotal: i64 = lines.iter().map(|l| l.subtotal_paise()).sum();
    let taxable = (subtotal - discount).max(0);
    let combined_rate = lines
        .first()
        .map(|l| l.tax_rate_percent)
        .unwrap_or(0.0);
    let tax = GstBreakdown::compute(taxable, combined_rate, inter_state);
    InvoiceTotals {
        subtotal,
        discount,
        taxable_value: tax.taxable_value_paise,
        cgst: tax.cgst_amount,
        sgst: tax.sgst_amount,
        igst: tax.igst_amount,
        total_tax: tax.total_tax_paise,
        total: tax.taxable_value_paise + tax.total_tax_paise,
    }
}

// ──────────────────────────────────────────────────────────────────────────
// Errors
// ──────────────────────────────────────────────────────────────────────────

#[derive(Debug, thiserror::Error)]
pub enum InvoiceError {
    #[error("database error: {0}")]
    Db(#[from] sqlx::Error),
    #[error("invoice not found")]
    NotFound,
    #[error("invalid amount: {0}")]
    InvalidAmount(i64),
    #[error("invoice already in status {0}")]
    IllegalState(String),
    #[error("invalid input: {0}")]
    InvalidInput(String),
}

pub type InvoiceResult<T> = Result<T, InvoiceError>;
