<?php

namespace App\Libraries;

use App\Models\BackupRunModel;
use Throwable;

/**
 * The shared dump logic BackupController's manual "Download Backup"
 * button already used, now also reusable by the scheduled `backup:run`
 * command and the "Run Backup Now" button — one real dump routine, not
 * two copies that could drift. Same "no shelling out to mysqldump"
 * reasoning as BackupController's own class doc comment: this runs
 * entirely through the app's own DB connection layer, since a hard
 * dependency on mysqldump being reachable is a real risk on shared
 * hosting with no shell access.
 *
 * runForCurrentTenant() is the actual automated-backup path: dumps the
 * current tenant's database, gzips it, writes it to
 * writable/backups/{slug}/ (outside the public web root, same as every
 * other writable/ file in this app), prunes local copies older than
 * RETENTION_DAYS, emails it as an attachment if the tenant has turned
 * that on (Settings > AI... no — Data Backup page's own "Automated
 * Backups" card, since this isn't an AI feature and Settings' three
 * tabs are all live-editable forms this destructive-adjacent feature
 * doesn't belong alongside — see BackupController's doc comment), and
 * always records the outcome to backup_runs regardless of success or
 * failure, so an unattended cron run is never silently invisible.
 */
class BackupService
{
    private const RETENTION_DAYS = 7;

    public function __construct(private readonly Settings $settings)
    {
    }

    public function dump($db): string
    {
        $tables = $db->listTables();
        $out    = "-- MaxYields tenant backup\n-- Generated " . date('c') . "\n\nSET FOREIGN_KEY_CHECKS=0;\n\n";

        foreach ($tables as $table) {
            $create = $db->query("SHOW CREATE TABLE `{$table}`")->getRow();
            $out .= "DROP TABLE IF EXISTS `{$table}`;\n";
            $out .= $create->{'Create Table'} . ";\n\n";

            $count     = $db->table($table)->countAllResults();
            $chunkSize = 500;

            for ($offset = 0; $offset < $count; $offset += $chunkSize) {
                $rows = $db->table($table)->limit($chunkSize, $offset)->get()->getResultArray();

                if (empty($rows)) {
                    continue;
                }

                $columns    = array_map(static fn ($c) => "`{$c}`", array_keys($rows[0]));
                $valueLines = [];

                foreach ($rows as $row) {
                    $values       = array_map(static fn ($v) => $db->escape($v), $row);
                    $valueLines[] = '(' . implode(',', $values) . ')';
                }

                $out .= 'INSERT INTO `' . $table . '` (' . implode(',', $columns) . ") VALUES\n" . implode(",\n", $valueLines) . ";\n";
            }

            $out .= "\n";
        }

        $out .= "SET FOREIGN_KEY_CHECKS=1;\n";

        return $out;
    }

    /**
     * @return array{success: bool, localPath: ?string, fileSizeBytes: ?int, emailed: bool, emailError: ?string, error: ?string}
     */
    public function runForCurrentTenant(?int $triggeredBy = null): array
    {
        $tenant = service('tenantContext')->getTenant();
        $runs   = new BackupRunModel();

        try {
            $db  = service('tenantContext')->getConnection();
            $sql = $this->dump($db);
            $gz  = gzencode($sql, 9);

            $dir = WRITEPATH . 'backups/' . $tenant->slug;

            if (! is_dir($dir)) {
                mkdir($dir, 0755, true);
            }

            $filename = 'backup-' . $tenant->slug . '-' . date('Y-m-d-His') . '.sql.gz';
            $path     = $dir . '/' . $filename;

            file_put_contents($path, $gz);
            $this->pruneOld($dir);

            $emailed    = false;
            $emailError = null;

            if ($this->settings->get('backup.email_enabled') === '1') {
                $recipient = $this->settings->get('backup.recipient_email');

                if ($recipient) {
                    $result = service('emailService')->send(
                        $recipient,
                        "Daily Backup — {$tenant->hotel_name} — " . date('d M Y'),
                        '<p>Attached is today\'s full database backup for ' . esc($tenant->hotel_name) . '.</p>'
                            . '<p style="color:#6b7280;font-size:0.85rem;">Automated — MaxYields PMS.</p>',
                        ['filename' => $filename, 'content' => $gz, 'mime' => 'application/gzip'],
                        null,
                    );

                    $emailed    = $result['success'];
                    $emailError = $result['success'] ? null : $result['error'];
                } else {
                    $emailError = 'Backup email is enabled but no recipient address is set.';
                }
            }

            $runs->insert([
                'status'          => 'success',
                'local_path'      => $path,
                'file_size_bytes' => strlen($gz),
                'emailed'         => $emailed ? 1 : 0,
                'email_error'     => $emailError,
                'triggered_by'    => $triggeredBy,
            ]);

            return ['success' => true, 'localPath' => $path, 'fileSizeBytes' => strlen($gz), 'emailed' => $emailed, 'emailError' => $emailError, 'error' => null];
        } catch (Throwable $e) {
            log_message('error', "BackupService: run failed for {$tenant->slug} — " . $e->getMessage());

            $runs->insert([
                'status'        => 'failed',
                'error_message' => $e->getMessage(),
                'triggered_by'  => $triggeredBy,
            ]);

            return ['success' => false, 'localPath' => null, 'fileSizeBytes' => null, 'emailed' => false, 'emailError' => null, 'error' => $e->getMessage()];
        }
    }

    /** Deletes local .sql.gz files in $dir older than RETENTION_DAYS — keeps the backups/ folder from growing forever. */
    private function pruneOld(string $dir): void
    {
        $cutoff = time() - self::RETENTION_DAYS * 86400;

        foreach (glob($dir . '/*.sql.gz') ?: [] as $file) {
            if (filemtime($file) < $cutoff) {
                unlink($file);
            }
        }
    }
}
