use crate::db::Database;
use crate::models::TransactionType;
use genpdf::elements;
use genpdf::style;
use genpdf::Element as _;
use std::path::Path;

const FONT_DIRS: &[&str] = &[
    "/usr/share/fonts/truetype/liberation",
    "/usr/share/fonts/liberation-sans",
    "/usr/share/fonts/TTF",
];

#[derive(Debug)]
pub enum PdfError {
    Db(rusqlite::Error),
    Pdf(genpdf::error::Error),
    FontNotFound,
}

impl From<rusqlite::Error> for PdfError {
    fn from(e: rusqlite::Error) -> Self {
        PdfError::Db(e)
    }
}

impl From<genpdf::error::Error> for PdfError {
    fn from(e: genpdf::error::Error) -> Self {
        PdfError::Pdf(e)
    }
}

impl std::fmt::Display for PdfError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PdfError::Db(e) => write!(f, "Database error: {}", e),
            PdfError::Pdf(e) => write!(f, "PDF error: {}", e),
            PdfError::FontNotFound => write!(f, "Liberation Sans font not found"),
        }
    }
}

fn find_font_dir() -> Option<&'static str> {
    for dir in FONT_DIRS {
        let path = Path::new(dir).join("LiberationSans-Regular.ttf");
        if path.exists() {
            return Some(dir);
        }
    }
    None
}

fn month_name(month: u32) -> &'static str {
    match month {
        1 => "January", 2 => "February", 3 => "March",
        4 => "April", 5 => "May", 6 => "June",
        7 => "July", 8 => "August", 9 => "September",
        10 => "October", 11 => "November", 12 => "December",
        _ => "Unknown",
    }
}

pub fn generate_monthly_report(
    db: &Database,
    year: i32,
    month: u32,
    base_currency: &str,
    output_path: &Path,
) -> Result<(), PdfError> {
    let font_dir = find_font_dir().ok_or(PdfError::FontNotFound)?;
    let font_family = genpdf::fonts::from_files(font_dir, "LiberationSans", None)?;

    let mut doc = genpdf::Document::new(font_family);
    doc.set_title("Outlay - Monthly Report");

    let mut decorator = genpdf::SimplePageDecorator::new();
    decorator.set_margins(15);
    doc.set_page_decorator(decorator);

    let title = format!(
        "Outlay - {} {} Report",
        month_name(month),
        year
    );
    doc.push(elements::Paragraph::new(&title).styled(style::Style::new().bold().with_font_size(18)));
    doc.push(elements::Break::new(1.5));

    let month_str = format!("{:04}-{:02}", year, month);
    let transactions = db.list_transactions_by_month(year, month)?;

    let mut total_income = 0.0_f64;
    let mut total_expenses = 0.0_f64;

    let mut category_totals: std::collections::HashMap<i64, (String, f64)> =
        std::collections::HashMap::new();

    for txn in &transactions {
        let converted = txn.amount * txn.exchange_rate;
        match txn.transaction_type {
            TransactionType::Income => total_income += converted,
            TransactionType::Expense => total_expenses += converted,
        }

        if txn.transaction_type == TransactionType::Expense {
            let cat_name = db
                .get_category(txn.category_id)
                .map(|c| c.name)
                .unwrap_or_else(|_| "Unknown".to_string());
            let entry = category_totals
                .entry(txn.category_id)
                .or_insert_with(|| (cat_name, 0.0));
            entry.1 += converted;
        }
    }

    let net = total_income - total_expenses;

    doc.push(elements::Paragraph::new("Summary").styled(style::Style::new().bold().with_font_size(14)));
    doc.push(elements::Break::new(0.5));

    doc.push(elements::Paragraph::new(format!(
        "Total Income:   {:.2} {}",
        total_income, base_currency
    )));
    doc.push(elements::Paragraph::new(format!(
        "Total Expenses: {:.2} {}",
        total_expenses, base_currency
    )));
    doc.push(elements::Paragraph::new(format!(
        "Net:            {:.2} {}",
        net, base_currency
    )));
    doc.push(elements::Break::new(1.0));

    if !category_totals.is_empty() {
        doc.push(elements::Paragraph::new("Expense Breakdown by Category").styled(
            style::Style::new().bold().with_font_size(14),
        ));
        doc.push(elements::Break::new(0.5));

        let mut table = elements::TableLayout::new(vec![3, 2, 1]);
        table.set_cell_decorator(elements::FrameCellDecorator::new(true, true, false));

        table
            .row()
            .element(elements::Paragraph::new("Category").styled(style::Style::new().bold()))
            .element(elements::Paragraph::new("Amount").styled(style::Style::new().bold()))
            .element(elements::Paragraph::new("% of Total").styled(style::Style::new().bold()))
            .push()
            .map_err(genpdf::error::Error::from)?;

        let mut sorted: Vec<(i64, (String, f64))> = category_totals.into_iter().collect();
        sorted.sort_by(|a, b| b.1 .1.partial_cmp(&a.1 .1).unwrap());

        for (_, (name, amount)) in &sorted {
            let pct = if total_expenses > 0.0 {
                amount / total_expenses * 100.0
            } else {
                0.0
            };
            table
                .row()
                .element(elements::Paragraph::new(name))
                .element(elements::Paragraph::new(format!("{:.2}", amount)))
                .element(elements::Paragraph::new(format!("{:.1}%", pct)))
                .push()
                .map_err(genpdf::error::Error::from)?;
        }

        doc.push(table);
        doc.push(elements::Break::new(1.0));
    }

    let mut expense_txns: Vec<_> = transactions
        .iter()
        .filter(|t| t.transaction_type == TransactionType::Expense)
        .collect();
    expense_txns.sort_by(|a, b| b.amount.partial_cmp(&a.amount).unwrap());

    if !expense_txns.is_empty() {
        doc.push(elements::Paragraph::new("Top Expenses").styled(
            style::Style::new().bold().with_font_size(14),
        ));
        doc.push(elements::Break::new(0.5));

        let mut table = elements::TableLayout::new(vec![2, 2, 2, 3]);
        table.set_cell_decorator(elements::FrameCellDecorator::new(true, true, false));

        table
            .row()
            .element(elements::Paragraph::new("Date").styled(style::Style::new().bold()))
            .element(elements::Paragraph::new("Category").styled(style::Style::new().bold()))
            .element(elements::Paragraph::new("Amount").styled(style::Style::new().bold()))
            .element(elements::Paragraph::new("Note").styled(style::Style::new().bold()))
            .push()
            .map_err(genpdf::error::Error::from)?;

        for txn in expense_txns.iter().take(5) {
            let cat_name = db
                .get_category(txn.category_id)
                .map(|c| c.name)
                .unwrap_or_else(|_| "Unknown".to_string());

            table
                .row()
                .element(elements::Paragraph::new(txn.date.format("%Y-%m-%d").to_string()))
                .element(elements::Paragraph::new(&cat_name))
                .element(elements::Paragraph::new(format!(
                    "{:.2} {}",
                    txn.amount, txn.currency
                )))
                .element(elements::Paragraph::new(
                    txn.note.as_deref().unwrap_or("-"),
                ))
                .push()
                .map_err(genpdf::error::Error::from)?;
        }

        doc.push(table);
        doc.push(elements::Break::new(1.0));
    }

    let budgets = db.list_budgets_for_month(&month_str)?;
    if !budgets.is_empty() {
        doc.push(elements::Paragraph::new("Budget vs Actual").styled(
            style::Style::new().bold().with_font_size(14),
        ));
        doc.push(elements::Break::new(0.5));

        let mut table = elements::TableLayout::new(vec![3, 2, 2, 1]);
        table.set_cell_decorator(elements::FrameCellDecorator::new(true, true, false));

        table
            .row()
            .element(elements::Paragraph::new("Category").styled(style::Style::new().bold()))
            .element(elements::Paragraph::new("Budget").styled(style::Style::new().bold()))
            .element(elements::Paragraph::new("Spent").styled(style::Style::new().bold()))
            .element(elements::Paragraph::new("Used").styled(style::Style::new().bold()))
            .push()
            .map_err(genpdf::error::Error::from)?;

        for budget in &budgets {
            let cat_name = db
                .get_category(budget.category_id)
                .map(|c| c.name)
                .unwrap_or_else(|_| "Unknown".to_string());

            let progress = db.get_budget_progress(budget.category_id, &month_str)?;
            let (budget_amt, spent, pct) = progress.unwrap_or((budget.amount, 0.0, 0.0));

            table
                .row()
                .element(elements::Paragraph::new(&cat_name))
                .element(elements::Paragraph::new(format!("{:.2}", budget_amt)))
                .element(elements::Paragraph::new(format!("{:.2}", spent)))
                .element(elements::Paragraph::new(format!("{:.0}%", pct)))
                .push()
                .map_err(genpdf::error::Error::from)?;
        }

        doc.push(table);
    }

    doc.push(elements::Break::new(1.5));
    doc.push(
        elements::Paragraph::new(format!("Generated by Outlay on {}", chrono::Local::now().format("%Y-%m-%d %H:%M")))
            .styled(style::Style::new().with_font_size(8)),
    );

    doc.render_to_file(output_path)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::models::{NewTransaction, TransactionType};
    use chrono::NaiveDate;

    fn setup_db() -> Database {
        Database::open_in_memory().unwrap()
    }

    #[test]
    fn test_pdf_report_generates_file() {
        if find_font_dir().is_none() {
            eprintln!("Skipping PDF test - Liberation Sans fonts not found");
            return;
        }

        let db = setup_db();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();
        let income_cats = db.list_categories(Some(TransactionType::Income)).unwrap();

        let txns = vec![
            NewTransaction {
                amount: 45.0,
                transaction_type: TransactionType::Expense,
                category_id: cats[0].id,
                currency: "USD".to_string(),
                exchange_rate: 1.0,
                note: Some("Groceries".to_string()),
                date: NaiveDate::from_ymd_opt(2026, 3, 1).unwrap(),
                recurring_id: None,
                payee: None,
            },
            NewTransaction {
                amount: 12.50,
                transaction_type: TransactionType::Expense,
                category_id: cats[1].id,
                currency: "USD".to_string(),
                exchange_rate: 1.0,
                note: Some("Coffee".to_string()),
                date: NaiveDate::from_ymd_opt(2026, 3, 5).unwrap(),
                recurring_id: None,
                payee: None,
            },
            NewTransaction {
                amount: 3000.0,
                transaction_type: TransactionType::Income,
                category_id: income_cats[0].id,
                currency: "USD".to_string(),
                exchange_rate: 1.0,
                note: Some("Salary".to_string()),
                date: NaiveDate::from_ymd_opt(2026, 3, 1).unwrap(),
                recurring_id: None,
                payee: None,
            },
        ];

        for txn in &txns {
            db.insert_transaction(txn).unwrap();
        }

        db.set_budget(cats[0].id, "2026-03", 200.0, false).unwrap();

        let tmp = std::env::temp_dir().join("outlay_test_report.pdf");
        generate_monthly_report(&db, 2026, 3, "USD", &tmp).unwrap();

        assert!(tmp.exists());
        let metadata = std::fs::metadata(&tmp).unwrap();
        assert!(metadata.len() > 100); // PDF should be non-trivial size

        std::fs::remove_file(&tmp).ok();
    }

    #[test]
    fn test_pdf_report_empty_month() {
        if find_font_dir().is_none() {
            eprintln!("Skipping PDF test - Liberation Sans fonts not found");
            return;
        }

        let db = setup_db();
        let tmp = std::env::temp_dir().join("outlay_test_empty_report.pdf");
        generate_monthly_report(&db, 2026, 1, "USD", &tmp).unwrap();

        assert!(tmp.exists());
        std::fs::remove_file(&tmp).ok();
    }
}
