use async_graphql::{Context, InputObject, Object, Result};
use chrono::NaiveDate;
use rust_decimal::Decimal;
use uuid::Uuid;

use crate::db::Database;
use crate::graphql::types::{InvoiceProjectType, InvoiceRevenueType, InvoiceStatusType, InvoiceType};
use crate::models::{EventType, Invoice, InvoiceProject, InvoiceRevenue, InvoiceStatus};
use crate::services::EventPublisher;
use crate::auth::UserContext;

/// Input for creating an invoice
#[derive(InputObject)]
pub struct CreateInvoiceInput {
    pub customer_id: Uuid,
    pub start_date: NaiveDate,
    pub end_date: NaiveDate,
}

/// Input for updating an invoice
#[derive(InputObject)]
pub struct UpdateInvoiceInput {
    pub start_date: Option<NaiveDate>,
    pub end_date: Option<NaiveDate>,
    pub status: Option<InvoiceStatusType>,
    pub date_paid: Option<NaiveDate>,
    pub wave_invoice_id: Option<String>,
}

#[derive(Default)]
pub struct InvoiceMutation;

#[Object]
impl InvoiceMutation {
    /// Create a new invoice for a customer
    async fn create_invoice(&self, ctx: &Context<'_>, input: CreateInvoiceInput) -> Result<InvoiceType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        // Validate date range
        if input.end_date < input.start_date {
            return Err("End date must be on or after start date".into());
        }

        let invoice: Invoice = sqlx::query_as::<_, Invoice>(
            r#"
            INSERT INTO invoices (
                id, created_at, updated_at, customer_id, start_date, end_date, status
            )
            VALUES (
                gen_random_uuid(), NOW(), NOW(), $1, $2, $3, 'DRAFT'
            )
            RETURNING id, created_at, updated_at, customer_id, start_date, end_date, status,
                      date_paid, payment_type, wave_invoice_id
            "#,
        )
        .bind(input.customer_id)
        .bind(input.start_date)
        .bind(input.end_date)
        .fetch_one(&*pool)
        .await?;

        // Publish event for audit trail
        if let Ok(user) = ctx.data::<UserContext>() {
            let _ = EventPublisher::publish(
                &pool,
                EventType::InvoiceCreated,
                "invoice",
                invoice.base.id,
                Some(("team_profile", user.user_id)),
                None,
            )
            .await;
        }

        Ok(InvoiceType::from(invoice))
    }

    /// Update an invoice
    async fn update_invoice(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
        input: UpdateInvoiceInput,
    ) -> Result<InvoiceType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        // Validate date range if both provided
        if let (Some(start), Some(end)) = (input.start_date, input.end_date) {
            if end < start {
                return Err("End date must be on or after start date".into());
            }
        }

        // Get old status for event tracking
        let old_status: Option<InvoiceStatus> = sqlx::query_scalar(
            "SELECT status FROM invoices WHERE id = $1",
        )
        .bind(id)
        .fetch_optional(&*pool)
        .await?;

        let new_status = input.status.map(InvoiceStatus::from);

        let invoice: Invoice = sqlx::query_as::<_, Invoice>(
            r#"
            UPDATE invoices
            SET
                start_date = COALESCE($2, start_date),
                end_date = COALESCE($3, end_date),
                status = COALESCE($4, status),
                date_paid = COALESCE($5, date_paid),
                wave_invoice_id = COALESCE($6, wave_invoice_id),
                updated_at = NOW()
            WHERE id = $1
            RETURNING id, created_at, updated_at, customer_id, start_date, end_date, status,
                      date_paid, payment_type, wave_invoice_id
            "#,
        )
        .bind(id)
        .bind(input.start_date)
        .bind(input.end_date)
        .bind(new_status)
        .bind(input.date_paid)
        .bind(input.wave_invoice_id)
        .fetch_one(&*pool)
        .await?;

        // Publish events for status changes
        if let Ok(user) = ctx.data::<UserContext>() {
            if let (Some(old), Some(new)) = (old_status, new_status) {
                if old != new {
                    let event_type = match new {
                        InvoiceStatus::Sent => Some(EventType::InvoiceSent),
                        InvoiceStatus::Paid => Some(EventType::InvoicePaid),
                        _ => None,
                    };

                    if let Some(event_type) = event_type {
                        let _ = EventPublisher::publish(
                            &pool,
                            event_type,
                            "invoice",
                            invoice.base.id,
                            Some(("team_profile", user.user_id)),
                            None,
                        )
                        .await;
                    }
                }
            }
        }

        Ok(InvoiceType::from(invoice))
    }

    /// Delete an invoice and all its entries
    async fn delete_invoice(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        // Delete entries first (cascade should handle this, but be explicit)
        sqlx::query("DELETE FROM invoice_revenues WHERE invoice_id = $1")
            .bind(id)
            .execute(&*pool)
            .await?;

        sqlx::query("DELETE FROM invoice_projects WHERE invoice_id = $1")
            .bind(id)
            .execute(&*pool)
            .await?;

        let result = sqlx::query("DELETE FROM invoices WHERE id = $1")
            .bind(id)
            .execute(&*pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }

    /// Add a revenue to an invoice with snapshot amount
    async fn add_revenue_to_invoice(
        &self,
        ctx: &Context<'_>,
        invoice_id: Uuid,
        revenue_id: Uuid,
    ) -> Result<InvoiceRevenueType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        // Get the invoice to verify customer
        let invoice = sqlx::query_as::<_, Invoice>(
            r#"
            SELECT id, created_at, updated_at, customer_id, start_date, end_date, status,
                   date_paid, payment_type, wave_invoice_id
            FROM invoices WHERE id = $1
            "#,
        )
        .bind(invoice_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or("Invoice not found")?;

        // Verify the revenue belongs to an account under this customer
        let revenue_info: Option<RevenueInfo> = sqlx::query_as::<_, RevenueInfo>(
            r#"
            SELECT r.amount, a.customer_id
            FROM revenues r
            JOIN accounts a ON a.id = r.account_id
            WHERE r.id = $1
            "#,
        )
        .bind(revenue_id)
        .fetch_optional(&*pool)
        .await?;

        let revenue_info = revenue_info.ok_or("Revenue not found")?;

        if revenue_info.customer_id != invoice.customer_id {
            return Err("Revenue does not belong to an account under this customer".into());
        }

        // Insert the invoice revenue with snapshot amount
        let entry: InvoiceRevenue = sqlx::query_as::<_, InvoiceRevenue>(
            r#"
            INSERT INTO invoice_revenues (id, invoice_id, revenue_id, amount, created_at)
            VALUES (gen_random_uuid(), $1, $2, $3, NOW())
            RETURNING id, invoice_id, revenue_id, amount, created_at
            "#,
        )
        .bind(invoice_id)
        .bind(revenue_id)
        .bind(revenue_info.amount)
        .fetch_one(&*pool)
        .await?;

        Ok(InvoiceRevenueType::from(entry))
    }

    /// Add a project to an invoice with snapshot amount
    async fn add_project_to_invoice(
        &self,
        ctx: &Context<'_>,
        invoice_id: Uuid,
        project_id: Uuid,
    ) -> Result<InvoiceProjectType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        // Get the invoice to verify customer
        let invoice = sqlx::query_as::<_, Invoice>(
            r#"
            SELECT id, created_at, updated_at, customer_id, start_date, end_date, status,
                   date_paid, payment_type, wave_invoice_id
            FROM invoices WHERE id = $1
            "#,
        )
        .bind(invoice_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or("Invoice not found")?;

        // Verify the project belongs to this customer and is completed
        let project_info: Option<ProjectInfo> = sqlx::query_as::<_, ProjectInfo>(
            r#"
            SELECT customer_id, amount, status
            FROM projects
            WHERE id = $1
            "#,
        )
        .bind(project_id)
        .fetch_optional(&*pool)
        .await?;

        let project_info = project_info.ok_or("Project not found")?;

        if project_info.customer_id != invoice.customer_id {
            return Err("Project does not belong to this customer".into());
        }

        if project_info.status != "COMPLETED" {
            return Err("Project must be completed before adding to invoice".into());
        }

        let amount = project_info.amount.unwrap_or(Decimal::ZERO);

        // Insert the invoice project with snapshot amount
        let entry: InvoiceProject = sqlx::query_as::<_, InvoiceProject>(
            r#"
            INSERT INTO invoice_projects (id, invoice_id, project_id, amount, created_at)
            VALUES (gen_random_uuid(), $1, $2, $3, NOW())
            RETURNING id, invoice_id, project_id, amount, created_at
            "#,
        )
        .bind(invoice_id)
        .bind(project_id)
        .bind(amount)
        .fetch_one(&*pool)
        .await?;

        Ok(InvoiceProjectType::from(entry))
    }

    /// Remove a revenue from an invoice
    async fn remove_revenue_from_invoice(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let result = sqlx::query("DELETE FROM invoice_revenues WHERE id = $1")
            .bind(id)
            .execute(&*pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }

    /// Remove a project from an invoice
    async fn remove_project_from_invoice(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let result = sqlx::query("DELETE FROM invoice_projects WHERE id = $1")
            .bind(id)
            .execute(&*pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }

    /// Add all eligible revenues to an invoice at once
    /// Returns the number of revenues added
    async fn add_all_eligible_revenues_to_invoice(
        &self,
        ctx: &Context<'_>,
        invoice_id: Uuid,
    ) -> Result<i32> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        // Get the invoice
        let invoice = sqlx::query_as::<_, Invoice>(
            r#"
            SELECT id, created_at, updated_at, customer_id, start_date, end_date, status,
                   date_paid, payment_type, wave_invoice_id
            FROM invoices WHERE id = $1
            "#,
        )
        .bind(invoice_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or("Invoice not found")?;

        // Find all eligible revenues and insert them
        // A revenue is eligible if it's active during the period and not already
        // on an invoice with an overlapping period
        let result = sqlx::query(
            r#"
            INSERT INTO invoice_revenues (id, invoice_id, revenue_id, amount, created_at)
            SELECT
                gen_random_uuid(),
                $1,
                r.id,
                r.amount,
                NOW()
            FROM revenues r
            JOIN accounts a ON a.id = r.account_id
            WHERE a.customer_id = $2
              AND r.start_date <= $4
              AND (r.end_date IS NULL OR r.end_date >= $3)
              AND NOT EXISTS (
                  SELECT 1 FROM invoice_revenues ir
                  JOIN invoices i ON i.id = ir.invoice_id
                  WHERE ir.revenue_id = r.id
                    AND i.start_date <= $4
                    AND i.end_date >= $3
              )
            "#,
        )
        .bind(invoice_id)
        .bind(invoice.customer_id)
        .bind(invoice.start_date)
        .bind(invoice.end_date)
        .execute(&*pool)
        .await?;

        Ok(result.rows_affected() as i32)
    }

    /// Add all eligible projects to an invoice at once
    /// Returns the number of projects added
    async fn add_all_eligible_projects_to_invoice(
        &self,
        ctx: &Context<'_>,
        invoice_id: Uuid,
    ) -> Result<i32> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        // Get the invoice
        let invoice = sqlx::query_as::<_, Invoice>(
            r#"
            SELECT id, created_at, updated_at, customer_id, start_date, end_date, status,
                   date_paid, payment_type, wave_invoice_id
            FROM invoices WHERE id = $1
            "#,
        )
        .bind(invoice_id)
        .fetch_optional(&*pool)
        .await?
        .ok_or("Invoice not found")?;

        // Find all eligible projects and insert them
        let result = sqlx::query(
            r#"
            INSERT INTO invoice_projects (id, invoice_id, project_id, amount, created_at)
            SELECT
                gen_random_uuid(),
                $1,
                p.id,
                COALESCE(p.amount, 0),
                NOW()
            FROM projects p
            WHERE p.customer_id = $2
              AND p.status = 'COMPLETED'
              AND p.date >= $3
              AND p.date <= $4
              AND NOT EXISTS (
                  SELECT 1 FROM invoice_projects ip WHERE ip.project_id = p.id
              )
            "#,
        )
        .bind(invoice_id)
        .bind(invoice.customer_id)
        .bind(invoice.start_date)
        .bind(invoice.end_date)
        .execute(&*pool)
        .await?;

        Ok(result.rows_affected() as i32)
    }
}

/// Helper struct for revenue info query
#[derive(sqlx::FromRow)]
struct RevenueInfo {
    amount: Decimal,
    customer_id: Uuid,
}

/// Helper struct for project info query
#[derive(sqlx::FromRow)]
struct ProjectInfo {
    customer_id: Uuid,
    amount: Option<Decimal>,
    status: String,
}
