<?php

declare(strict_types=1);

namespace Budget;

use PDO;
use RuntimeException;

final class ExportService
{
    private const TABLES = [
        'monthly_plans', 'income_entries', 'categories', 'budget_items',
        'transactions', 'dead_account_entries', 'calendar_events',
    ];

    public function __construct(private PDO $pdo, private Settings $settings, private BudgetService $budget)
    {
    }

    public function package(): string
    {
        $data = [];
        foreach (self::TABLES as $table) {
            $data[$table] = $this->pdo->query('SELECT * FROM ' . $table)->fetchAll();
        }
        $safeSettings = [];
        foreach ($this->settings->all() as $key => $value) {
            if (!str_starts_with($key, 'mailgun.api_key') && !str_starts_with($key, 'security.')) {
                $safeSettings[$key] = $value;
            }
        }
        $package = [
            'format' => 'neon-ledger/budget-planner',
            'version' => 1,
            'exported_at' => date(DATE_ATOM),
            'settings' => $safeSettings,
            'data' => $data,
        ];
        $json = json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if ($json === false) {
            throw new RuntimeException('Planner package could not be encoded.');
        }
        return $json;
    }

    public function importPackage(string $json, bool $replace): array
    {
        $package = json_decode($json, true);
        if (!is_array($package)
            || ($package['format'] ?? '') !== 'neon-ledger/budget-planner'
            || (int) ($package['version'] ?? 0) !== 1
            || !is_array($package['data'] ?? null)
        ) {
            throw new RuntimeException('This is not a compatible Neon Ledger planner package.');
        }

        $inserted = [];
        $this->pdo->beginTransaction();
        try {
            if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql') {
                $this->pdo->exec('SET FOREIGN_KEY_CHECKS=0');
            } else {
                $this->pdo->exec('PRAGMA defer_foreign_keys = ON');
            }
            if ($replace) {
                foreach (array_reverse(self::TABLES) as $table) {
                    $this->pdo->exec('DELETE FROM ' . $table);
                }
            }
            foreach (self::TABLES as $table) {
                $rows = $package['data'][$table] ?? [];
                $inserted[$table] = 0;
                if (!is_array($rows)) {
                    continue;
                }
                foreach ($rows as $row) {
                    if (!is_array($row) || $row === []) {
                        continue;
                    }
                    $columns = array_keys($row);
                    $columnSql = implode(', ', array_map(
                        fn (string $column): string => $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql'
                            ? '`' . $column . '`'
                            : '"' . $column . '"',
                        $columns
                    ));
                    $placeholders = implode(', ', array_map(static fn (string $column): string => ':' . $column, $columns));
                    $verb = $replace
                        ? ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? 'REPLACE' : 'INSERT OR REPLACE')
                        : ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? 'INSERT IGNORE' : 'INSERT OR IGNORE');
                    $statement = $this->pdo->prepare(sprintf('%s INTO %s (%s) VALUES (%s)', $verb, $table, $columnSql, $placeholders));
                    $statement->execute($row);
                    $inserted[$table] += $statement->rowCount();
                }
            }
            foreach ((array) ($package['settings'] ?? []) as $key => $value) {
                if (is_string($key) && !str_starts_with($key, 'mailgun.api_key') && !str_starts_with($key, 'security.')) {
                    $this->settings->set($key, $value);
                }
            }
            if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql') {
                $this->pdo->exec('SET FOREIGN_KEY_CHECKS=1');
            }
            $this->pdo->commit();
        } catch (\Throwable $exception) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $exception;
        }
        return $inserted;
    }

    public function html(string $month): string
    {
        $summary = $this->budget->summary($month);
        $items = $this->budget->budgetItems($month);
        $transactions = $this->budget->transactions($month);
        $symbol = (string) $this->settings->get('app.currency_symbol', '$');
        $escape = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
        $money = static fn (mixed $value): string => Support::money((float) $value, $symbol);

        $itemRows = '';
        foreach ($items as $item) {
            $itemRows .= sprintf(
                '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>',
                $escape($item['name']),
                $escape($item['category_name'] ?: 'Uncategorized'),
                $money($item['planned_amount']),
                $money($item['paid_amount']),
                $money($item['remaining_amount'])
            );
        }
        $transactionRows = '';
        foreach ($transactions as $transaction) {
            $transactionRows .= sprintf(
                '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>',
                $escape($transaction['transacted_on']),
                $escape($transaction['merchant']),
                $escape($transaction['category_name'] ?: 'Uncategorized'),
                $money($transaction['amount'])
            );
        }

        return '<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Budget report ' . $escape($month) . '</title>'
            . '<style>body{font:15px/1.5 system-ui;color:#182033;max-width:1000px;margin:40px auto;padding:0 24px}h1{color:#5b36d9}'
            . '.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.stat{border:1px solid #ddd;border-radius:10px;padding:14px}'
            . 'table{width:100%;border-collapse:collapse;margin:20px 0}th,td{text-align:left;padding:9px;border-bottom:1px solid #ddd}th{color:#5b36d9}'
            . '@media print{body{margin:0}.no-print{display:none}}</style></head><body>'
            . '<button class="no-print" onclick="print()">Print / Save as PDF</button>'
            . '<h1>' . $escape($this->settings->get('app.name', 'Neon Ledger')) . ' &mdash; ' . $escape(date('F Y', strtotime($month . '-01'))) . '</h1>'
            . '<div class="stats"><div class="stat"><small>Resources</small><strong>' . $money($summary['gross_resources']) . '</strong></div>'
            . '<div class="stat"><small>Expenses</small><strong>' . $money($summary['expenses']) . '</strong></div>'
            . '<div class="stat"><small>Dormant movement</small><strong>' . $money($summary['dead_movement']) . '</strong></div>'
            . '<div class="stat"><small>Remaining</small><strong>' . $money($summary['remaining']) . '</strong></div></div>'
            . '<h2>Budget progress</h2><table><thead><tr><th>Item</th><th>Category</th><th>Target</th><th>Paid</th><th>Owing</th></tr></thead><tbody>'
            . $itemRows . '</tbody></table><h2>Expenses</h2><table><thead><tr><th>Date</th><th>Description</th><th>Category</th><th>Amount</th></tr></thead><tbody>'
            . $transactionRows . '</tbody></table><p>Exported ' . $escape(date(DATE_RFC2822)) . '</p></body></html>';
    }

    public function pdf(string $month): string
    {
        $summary = $this->budget->summary($month);
        $items = $this->budget->budgetItems($month);
        $symbol = (string) $this->settings->get('app.currency_symbol', '$');
        $lines = [
            (string) $this->settings->get('app.name', 'Neon Ledger') . ' - ' . date('F Y', strtotime($month . '-01')),
            '',
            'Resources: ' . Support::money($summary['gross_resources'], $symbol),
            'Expenses: ' . Support::money($summary['expenses'], $symbol),
            'Dormant movement: ' . Support::money($summary['dead_movement'], $symbol),
            'Remaining: ' . Support::money($summary['remaining'], $symbol),
            '',
            'BUDGET PROGRESS',
        ];
        foreach ($items as $item) {
            $lines[] = sprintf(
                '%s | target %s | paid %s | owing %s',
                $item['name'],
                Support::money($item['planned_amount'], $symbol),
                Support::money($item['paid_amount'], $symbol),
                Support::money($item['remaining_amount'], $symbol)
            );
        }
        $lines[] = '';
        $lines[] = 'Generated ' . date(DATE_RFC2822);
        return $this->simplePdf($lines);
    }

    private function simplePdf(array $lines): string
    {
        $content = "BT\n/F1 11 Tf\n50 760 Td\n14 TL\n";
        foreach ($lines as $index => $line) {
            $line = iconv('UTF-8', 'Windows-1252//TRANSLIT', (string) $line) ?: (string) $line;
            $line = str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], $line);
            $content .= ($index === 0 ? '' : "T*\n") . '(' . $line . ") Tj\n";
        }
        $content .= "ET\n";
        $objects = [
            '<< /Type /Catalog /Pages 2 0 R >>',
            '<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
            '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>',
            '<< /Length ' . strlen($content) . " >>\nstream\n" . $content . 'endstream',
            '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
        ];
        $pdf = "%PDF-1.4\n";
        $offsets = [0];
        foreach ($objects as $number => $object) {
            $offsets[] = strlen($pdf);
            $pdf .= ($number + 1) . " 0 obj\n" . $object . "\nendobj\n";
        }
        $xref = strlen($pdf);
        $pdf .= "xref\n0 " . (count($objects) + 1) . "\n0000000000 65535 f \n";
        for ($index = 1; $index <= count($objects); $index++) {
            $pdf .= sprintf("%010d 00000 n \n", $offsets[$index]);
        }
        $pdf .= 'trailer << /Size ' . (count($objects) + 1) . " /Root 1 0 R >>\nstartxref\n" . $xref . "\n%%EOF";
        return $pdf;
    }
}
