//! Invoice service: create, read, void, and PDF upload.
//!
//! The service is the single point of contact for the `invoices` table
//! and the corresponding `invoice_line_items`. It is built around an
//! `InvoiceRepository` so it can be unit-tested with an in-memory pool
//! and so the heavy logic does not depend on axum.

use crate::{
    BillingDetails, GstBreakdown, Invoice, InvoiceError, InvoiceResult, InvoiceTotals,
    LineItem, SellerDetails, format_invoice_number,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool, Postgres, Row, Transaction};
use uuid::Uuid;

// ──────────────────────────────────────────────────────────────────────────
// Request DTOs
// ──────────────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewInvoice {
    pub payment_id: Uuid,
    pub user_id: Uuid,
    pub currency: String,
    pub invoice_type: String,
    pub lines: Vec<LineItem>,
    pub discount_amount: i64,
    pub discount_label: Option<String>,
    pub notes: Option<String>,
    pub customer: BillingDetails,
    pub seller: SellerDetails,
    pub inter_state: bool,
    pub pdf_object_key: Option<String>,
}

// ──────────────────────────────────────────────────────────────────────────
// Database row
// ──────────────────────────────────────────────────────────────────────────

#[derive(Debug, FromRow)]
pub struct InvoiceRow {
    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: i32,
    pub discount_amount: i32,
    pub cgst_rate: f64,
    pub cgst_amount: i32,
    pub sgst_rate: f64,
    pub sgst_amount: i32,
    pub igst_rate: f64,
    pub igst_amount: i32,
    pub total: i32,
    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>,
}

impl From<InvoiceRow> for Invoice {
    fn from(r: InvoiceRow) -> Self {
        Invoice {
            id: r.id,
            invoice_number: r.invoice_number,
            payment_id: r.payment_id,
            user_id: r.user_id,
            status: r.status,
            currency: r.currency,
            invoice_type: r.invoice_type,
            subtotal: r.subtotal as i64,
            discount_amount: r.discount_amount as i64,
            cgst_rate: r.cgst_rate,
            cgst_amount: r.cgst_amount as i64,
            sgst_rate: r.sgst_rate,
            sgst_amount: r.sgst_amount as i64,
            igst_rate: r.igst_rate,
            igst_amount: r.igst_amount as i64,
            total: r.total as i64,
            reverse_charge: r.reverse_charge,
            seller_name: r.seller_name,
            seller_address: r.seller_address,
            seller_gstin: r.seller_gstin,
            seller_pan: r.seller_pan,
            seller_state_code: r.seller_state_code,
            place_of_supply_state: r.place_of_supply_state,
            customer_name: r.customer_name,
            customer_email: r.customer_email,
            customer_phone: r.customer_phone,
            customer_billing_address: r.customer_billing_address,
            customer_gstin: r.customer_gstin,
            customer_state_code: r.customer_state_code,
            discount_label: r.discount_label,
            notes: r.notes,
            pdf_object_key: r.pdf_object_key,
            issued_at: r.issued_at,
            paid_at: r.paid_at,
            voided_at: r.voided_at,
            voided_by_user_id: r.voided_by_user_id,
            void_reason: r.void_reason,
            created_at: r.created_at,
            updated_at: r.updated_at,
        }
    }
}

#[derive(Debug, FromRow)]
pub struct LineItemRow {
    pub id: Uuid,
    pub invoice_id: Uuid,
    pub line_number: i32,
    pub description: String,
    pub hsn_sac_code: Option<String>,
    pub quantity: f64,
    pub unit_price_inr: i32,
    pub line_subtotal_inr: i32,
    pub tax_rate: f64,
    pub line_tax_inr: i32,
    pub line_total_inr: i32,
    pub metadata: Option<serde_json::Value>,
}

impl From<LineItemRow> for LineItem {
    fn from(r: LineItemRow) -> Self {
        LineItem {
            line_number: r.line_number,
            description: r.description,
            hsn_sac_code: r.hsn_sac_code,
            quantity: r.quantity,
            unit_price_paise: r.unit_price_inr as i64,
            tax_rate_percent: r.tax_rate,
            metadata: r.metadata,
        }
    }
}

// ──────────────────────────────────────────────────────────────────────────
// Service
// ──────────────────────────────────────────────────────────────────────────

pub struct InvoiceService;

impl InvoiceService {
    /// Create a new invoice. The invoice number is allocated atomically
    /// from the `invoice_number_seq` sequence, so concurrent calls do not
    /// race the same number.
    pub async fn create(
        pool: &PgPool,
        new: NewInvoice,
    ) -> InvoiceResult<Invoice> {
        // Basic validation.
        if new.lines.is_empty() {
            return Err(InvoiceError::InvalidInput(
                "invoice must have at least one line item".to_string(),
            ));
        }
        if new.discount_amount < 0 {
            return Err(InvoiceError::InvalidAmount(new.discount_amount));
        }
        if new.customer.legal_name.trim().is_empty() {
            return Err(InvoiceError::InvalidInput(
                "customer.legal_name is required".to_string(),
            ));
        }
        if new.seller.name.trim().is_empty() {
            return Err(InvoiceError::InvalidInput(
                "seller.name is required".to_string(),
            ));
        }

        // Compute totals.
        let combined_rate = new.lines[0].tax_rate_percent;
        let subtotal: i64 = new.lines.iter().map(|l| l.subtotal_paise()).sum();
        let taxable = (subtotal - new.discount_amount).max(0);
        let tax = GstBreakdown::compute(taxable, combined_rate, new.inter_state);
        let total = taxable + tax.total_tax_paise;
        let totals = InvoiceTotals {
            subtotal,
            discount: new.discount_amount,
            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,
        };

        let mut tx = pool.begin().await?;

        // Allocate next sequence number and format the invoice number.
        let seq: i64 = sqlx::query_scalar("SELECT nextval('invoice_number_seq')")
            .fetch_one(&mut *tx)
            .await?;
        let invoice_number = format_invoice_number(
            Utc::now().format("%Y").to_string().parse().unwrap_or(2026),
            new.customer.state_code.as_deref(),
            seq,
        );

        // Insert the invoice row.
        let invoice_id = Uuid::new_v4();
        sqlx::query(
            r#"
            INSERT INTO invoices (
                id, invoice_number, payment_id, user_id, status, currency,
                invoice_type, subtotal, discount_amount, discount_label, notes,
                cgst_rate, cgst_amount, sgst_rate, sgst_amount, igst_rate, igst_amount,
                total, reverse_charge,
                seller_name, seller_address, seller_gstin, seller_pan, seller_state_code,
                place_of_supply_state,
                customer_name, customer_email, customer_phone,
                customer_billing_address, customer_gstin, customer_state_code,
                pdf_object_key, issued_at
            ) VALUES (
                $1, $2, $3, $4, 'ISSUED', $5, $6, $7, $8, $9, $10,
                $11, $12, $13, $14, $15, $16, $17, $18,
                $19, $20, $21, $22, $23, $24,
                $25, $26, $27, $28, $29, $30, $31, NOW()
            )
            "#,
        )
        .bind(invoice_id)
        .bind(&invoice_number)
        .bind(new.payment_id)
        .bind(new.user_id)
        .bind(&new.currency)
        .bind(&new.invoice_type)
        .bind(totals.subtotal as i32)
        .bind(totals.discount as i32)
        .bind(new.discount_label.as_deref())
        .bind(new.notes.as_deref())
        .bind(tax.cgst_rate)
        .bind(tax.cgst_amount as i32)
        .bind(tax.sgst_rate)
        .bind(tax.sgst_amount as i32)
        .bind(tax.igst_rate)
        .bind(tax.igst_amount as i32)
        .bind(totals.total as i32)
        .bind(new.inter_state)
        .bind(&new.seller.name)
        .bind(&new.seller.address)
        .bind(new.seller.gstin.as_deref())
        .bind(new.seller.pan.as_deref())
        .bind(&new.seller.state_code)
        .bind(new.customer.state_code.as_deref())
        .bind(&new.customer.legal_name)
        .bind(new.customer.email.as_deref())
        .bind(new.customer.phone.as_deref())
        .bind(&new.customer.billing_address)
        .bind(new.customer.gstin.as_deref())
        .bind(new.customer.state_code.as_deref())
        .bind(new.pdf_object_key.as_deref())
        .execute(&mut *tx)
        .await?;

        // Insert line items.
        for line in &new.lines {
            sqlx::query(
                r#"
                INSERT INTO invoice_line_items (
                    invoice_id, line_number, description, hsn_sac_code,
                    quantity, unit_price_inr, line_subtotal_inr,
                    tax_rate, line_tax_inr, line_total_inr, metadata
                ) VALUES (
                    $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
                )
                "#,
            )
            .bind(invoice_id)
            .bind(line.line_number)
            .bind(&line.description)
            .bind(line.hsn_sac_code.as_deref())
            .bind(line.quantity)
            .bind(line.unit_price_paise as i32)
            .bind(line.subtotal_paise() as i32)
            .bind(line.tax_rate_percent)
            .bind(line.tax_paise() as i32)
            .bind(line.total_paise() as i32)
            .bind(line.metadata.as_ref())
            .execute(&mut *tx)
            .await?;
        }

        tx.commit().await?;

        Self::get(pool, invoice_id)
            .await?
            .ok_or(InvoiceError::NotFound)
    }

    pub async fn get(
        pool: &PgPool,
        id: Uuid,
    ) -> InvoiceResult<Option<Invoice>> {
        let row = sqlx::query_as::<_, InvoiceRow>(
            r#"
            SELECT
                id, invoice_number, payment_id, user_id, status, currency,
                invoice_type, subtotal, discount_amount,
                cgst_rate::FLOAT8 AS cgst_rate, cgst_amount,
                sgst_rate::FLOAT8 AS sgst_rate, sgst_amount,
                igst_rate::FLOAT8 AS igst_rate, igst_amount, total,
                reverse_charge, seller_name, seller_address, seller_gstin,
                seller_pan, seller_state_code, place_of_supply_state,
                customer_name, customer_email, customer_phone,
                customer_billing_address, customer_gstin, customer_state_code,
                discount_label, notes, pdf_object_key, issued_at, paid_at,
                voided_at, voided_by_user_id, void_reason, created_at, updated_at
            FROM invoices
            WHERE id = $1
            "#,
        )
        .bind(id)
        .fetch_optional(pool)
        .await?;
        Ok(row.map(Into::into))
    }

    pub async fn get_by_payment(
        pool: &PgPool,
        payment_id: Uuid,
    ) -> InvoiceResult<Option<Invoice>> {
        let row = sqlx::query_as::<_, InvoiceRow>(
            r#"
            SELECT
                id, invoice_number, payment_id, user_id, status, currency,
                invoice_type, subtotal, discount_amount,
                cgst_rate::FLOAT8 AS cgst_rate, cgst_amount,
                sgst_rate::FLOAT8 AS sgst_rate, sgst_amount,
                igst_rate::FLOAT8 AS igst_rate, igst_amount, total,
                reverse_charge, seller_name, seller_address, seller_gstin,
                seller_pan, seller_state_code, place_of_supply_state,
                customer_name, customer_email, customer_phone,
                customer_billing_address, customer_gstin, customer_state_code,
                discount_label, notes, pdf_object_key, issued_at, paid_at,
                voided_at, voided_by_user_id, void_reason, created_at, updated_at
            FROM invoices
            WHERE payment_id = $1
            ORDER BY issued_at DESC
            LIMIT 1
            "#,
        )
        .bind(payment_id)
        .fetch_optional(pool)
        .await?;
        Ok(row.map(Into::into))
    }

    pub async fn list_for_user(
        pool: &PgPool,
        user_id: Uuid,
        page: i64,
        limit: i64,
    ) -> InvoiceResult<Vec<Invoice>> {
        let limit = limit.clamp(1, 200);
        let page = page.max(1);
        let offset = (page - 1) * limit;

        let rows = sqlx::query_as::<_, InvoiceRow>(
            r#"
            SELECT
                id, invoice_number, payment_id, user_id, status, currency,
                invoice_type, subtotal, discount_amount,
                cgst_rate::FLOAT8 AS cgst_rate, cgst_amount,
                sgst_rate::FLOAT8 AS sgst_rate, sgst_amount,
                igst_rate::FLOAT8 AS igst_rate, igst_amount, total,
                reverse_charge, seller_name, seller_address, seller_gstin,
                seller_pan, seller_state_code, place_of_supply_state,
                customer_name, customer_email, customer_phone,
                customer_billing_address, customer_gstin, customer_state_code,
                discount_label, notes, pdf_object_key, issued_at, paid_at,
                voided_at, voided_by_user_id, void_reason, created_at, updated_at
            FROM invoices
            WHERE user_id = $1
            ORDER BY issued_at DESC
            LIMIT $2 OFFSET $3
            "#,
        )
        .bind(user_id)
        .bind(limit)
        .bind(offset)
        .fetch_all(pool)
        .await?;

        Ok(rows.into_iter().map(Into::into).collect())
    }

    pub async fn list_all(
        pool: &PgPool,
        status: Option<&str>,
        page: i64,
        limit: i64,
    ) -> InvoiceResult<Vec<Invoice>> {
        let limit = limit.clamp(1, 200);
        let page = page.max(1);
        let offset = (page - 1) * limit;

        let status_filter = status.map(str::to_ascii_uppercase);
        let rows = sqlx::query_as::<_, InvoiceRow>(
            r#"
            SELECT
                id, invoice_number, payment_id, user_id, status, currency,
                invoice_type, subtotal, discount_amount,
                cgst_rate::FLOAT8 AS cgst_rate, cgst_amount,
                sgst_rate::FLOAT8 AS sgst_rate, sgst_amount,
                igst_rate::FLOAT8 AS igst_rate, igst_amount, total,
                reverse_charge, seller_name, seller_address, seller_gstin,
                seller_pan, seller_state_code, place_of_supply_state,
                customer_name, customer_email, customer_phone,
                customer_billing_address, customer_gstin, customer_state_code,
                discount_label, notes, pdf_object_key, issued_at, paid_at,
                voided_at, voided_by_user_id, void_reason, created_at, updated_at
            FROM invoices
            WHERE ($1::text IS NULL OR status = $1)
            ORDER BY issued_at DESC
            LIMIT $2 OFFSET $3
            "#,
        )
        .bind(status_filter)
        .bind(limit)
        .bind(offset)
        .fetch_all(pool)
        .await?;

        Ok(rows.into_iter().map(Into::into).collect())
    }

    pub async fn list_line_items(
        pool: &PgPool,
        invoice_id: Uuid,
    ) -> InvoiceResult<Vec<LineItem>> {
        let rows = sqlx::query_as::<_, LineItemRow>(
            r#"
            SELECT
                id, invoice_id, line_number, description, hsn_sac_code,
                quantity::FLOAT8 AS quantity, unit_price_inr, line_subtotal_inr,
                tax_rate::FLOAT8 AS tax_rate, line_tax_inr, line_total_inr, metadata
            FROM invoice_line_items
            WHERE invoice_id = $1
            ORDER BY line_number ASC
            "#,
        )
        .bind(invoice_id)
        .fetch_all(pool)
        .await?;
        Ok(rows.into_iter().map(Into::into).collect())
    }

    pub async fn mark_paid(
        pool: &PgPool,
        id: Uuid,
    ) -> InvoiceResult<()> {
        let result = sqlx::query(
            r#"
            UPDATE invoices
            SET status = 'PAID', paid_at = NOW(), updated_at = NOW()
            WHERE id = $1 AND status = 'ISSUED'
            "#,
        )
        .bind(id)
        .execute(pool)
        .await?;
        if result.rows_affected() == 0 {
            return Err(InvoiceError::IllegalState("not ISSUED".to_string()));
        }
        Ok(())
    }

    pub async fn void(
        pool: &PgPool,
        id: Uuid,
        by_user_id: Uuid,
        reason: &str,
    ) -> InvoiceResult<()> {
        if reason.trim().is_empty() {
            return Err(InvoiceError::InvalidInput("void reason is required".to_string()));
        }
        let result = sqlx::query(
            r#"
            UPDATE invoices
            SET status = 'VOID', voided_at = NOW(), voided_by_user_id = $2,
                void_reason = $3, updated_at = NOW()
            WHERE id = $1 AND status IN ('ISSUED', 'PAID')
            "#,
        )
        .bind(id)
        .bind(by_user_id)
        .bind(reason)
        .execute(pool)
        .await?;
        if result.rows_affected() == 0 {
            return Err(InvoiceError::IllegalState("not ISSUED or PAID".to_string()));
        }
        Ok(())
    }

    pub async fn attach_pdf(
        pool: &PgPool,
        id: Uuid,
        object_key: &str,
    ) -> InvoiceResult<()> {
        sqlx::query(
            r#"
            UPDATE invoices
            SET pdf_object_key = $2, updated_at = NOW()
            WHERE id = $1
            "#,
        )
        .bind(id)
        .bind(object_key)
        .execute(pool)
        .await?;
        Ok(())
    }

    /// Compute totals from a stored invoice + its line items.
    pub fn totals(invoice: &Invoice, lines: &[LineItem]) -> InvoiceTotals {
        crate::compute_totals(lines, invoice.discount_amount, invoice.igst_amount > 0)
    }
}

// ──────────────────────────────────────────────────────────────────────────
// Billing profile repository
// ──────────────────────────────────────────────────────────────────────────

pub struct BillingProfileRepo;

impl BillingProfileRepo {
    pub async fn upsert_default(
        pool: &PgPool,
        user_id: Uuid,
        profile: &BillingDetails,
    ) -> InvoiceResult<()> {
        let mut tx = pool.begin().await?;
        // Clear existing default.
        sqlx::query(
            r#"
            UPDATE billing_profiles
            SET is_default = false, updated_at = NOW()
            WHERE user_id = $1 AND is_default = true
            "#,
        )
        .bind(user_id)
        .execute(&mut *tx)
        .await?;
        // Insert new default.
        sqlx::query(
            r#"
            INSERT INTO billing_profiles (
                user_id, legal_name, email, phone, gstin, pan,
                billing_address, state_code, is_default
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true)
            "#,
        )
        .bind(user_id)
        .bind(&profile.legal_name)
        .bind(profile.email.as_deref())
        .bind(profile.phone.as_deref())
        .bind(profile.gstin.as_deref())
        .bind(profile.pan.as_deref())
        .bind(&profile.billing_address)
        .bind(profile.state_code.as_deref())
        .execute(&mut *tx)
        .await?;
        tx.commit().await?;
        Ok(())
    }

    pub async fn get_default(
        pool: &PgPool,
        user_id: Uuid,
    ) -> InvoiceResult<Option<BillingDetails>> {
        let row: Option<(String, Option<String>, Option<String>, Option<String>, Option<String>, String, Option<String>)> =
            sqlx::query_as(
                r#"
                SELECT legal_name, email, phone, gstin, pan, billing_address, state_code
                FROM billing_profiles
                WHERE user_id = $1 AND is_default = true
                "#,
            )
            .bind(user_id)
            .fetch_optional(pool)
            .await?;
        Ok(row.map(|(n, e, p, g, pa, a, s)| BillingDetails {
            legal_name: n,
            email: e,
            phone: p,
            gstin: g,
            pan: pa,
            billing_address: a,
            state_code: s,
        }))
    }
}

// Suppress unused warning for Transaction import in non-tx code paths.
#[allow(dead_code)]
fn _phantom_tx<'a>(_: &Transaction<'a, Postgres>) {}
