<?php

namespace App\Controllers;

use App\Models\BackupRunModel;
use Throwable;

/**
 * Tenant-scoped database backup/restore, manager-only. Deliberately not a
 * Settings tab — Settings' three tabs are all live-editable autosave
 * forms, and a destructive, typed-confirmation, forced-download action
 * doesn't fit that pattern or that UX. The "Automated Backups" card
 * below (email toggle + recipient) is a live-editable form and could
 * have lived in Settings, but stays here instead — everything backup-
 * related in one place rather than split across two screens.
 *
 * Manual export/restore run entirely in-process through this app's own
 * DB connection layer (see BackupService::dump()) rather than shelling
 * out to mysqldump/mysql — nothing in this codebase shells out to
 * anything today, and a hard dependency on those binaries being
 * reachable at runtime is a real risk if this ever moves to shared/
 * managed hosting without shell access.
 *
 * Everything here operates ONLY on the current tenant's own database —
 * never another tenant's, never the control DB.
 */
class BackupController extends BaseController
{
    private const CONFIRM_PHRASE = 'RESTORE';

    public function index()
    {
        $settings = service('settings');

        return view('backup/index', [
            'pageTitle'      => 'Data Backup',
            'tenant'         => service('tenantContext')->getTenant(),
            'emailEnabled'   => $settings->get('backup.email_enabled') === '1',
            'recipientEmail' => $settings->get('backup.recipient_email'),
            'runs'           => (new BackupRunModel())->recent(10),
        ]);
    }

    /** The "Automated Backups" card — email toggle + recipient, saved independently of the destructive actions below. */
    public function updateSettings()
    {
        $settings  = service('settings');
        $enabled   = $this->request->getPost('email_enabled') ? '1' : '0';
        $recipient = trim((string) $this->request->getPost('recipient_email'));

        if ($enabled === '1' && $recipient !== '' && ! filter_var($recipient, FILTER_VALIDATE_EMAIL)) {
            return redirect()->to('backup')->with('error', 'Enter a valid recipient email address.');
        }

        $settings->set('backup.email_enabled', $enabled);
        $settings->set('backup.recipient_email', $recipient ?: null);

        $this->logActivity('backup', 'Updated automated backup settings.');

        return redirect()->to('backup')->with('success', 'Backup settings saved.');
    }

    /** Runs the exact same path `php spark backup:run` uses for this tenant, on demand — good for testing without waiting for the schedule. */
    public function runNow()
    {
        $result = service('backupService')->runForCurrentTenant($this->currentUserId());

        $this->logActivity('backup', $result['success']
            ? 'Ran a manual backup (' . round(($result['fileSizeBytes'] ?? 0) / 1024, 1) . ' KB).'
            : 'Manual backup FAILED: ' . $result['error']);

        if (! $result['success']) {
            return redirect()->to('backup')->with('error', 'Backup failed: ' . $result['error']);
        }

        $message = 'Backup complete (' . round($result['fileSizeBytes'] / 1024, 1) . ' KB).';

        if ($result['emailed']) {
            $message .= ' Emailed successfully.';
        } elseif ($result['emailError']) {
            $message .= ' Email not sent: ' . $result['emailError'];
        }

        return redirect()->to('backup')->with('success', $message);
    }

    public function export()
    {
        $db     = service('tenantContext')->getConnection();
        $tenant = service('tenantContext')->getTenant();

        $sql      = service('backupService')->dump($db);
        $filename = 'backup-' . $tenant->slug . '-' . date('Y-m-d-His') . '.sql';

        $this->logActivity('backup', "Downloaded a full database backup ({$filename}).");

        return $this->response
            ->setHeader('Content-Type', 'application/sql')
            ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
            ->setBody($sql);
    }

    public function restore()
    {
        $confirm = trim((string) $this->request->getPost('confirm_phrase'));

        if ($confirm !== self::CONFIRM_PHRASE) {
            return redirect()->to('backup')->with('error', 'Confirmation phrase did not match — restore cancelled.');
        }

        $file = $this->request->getFile('backup_file');

        if ($file === null || ! $file->isValid()) {
            return redirect()->to('backup')->with('error', 'Choose a valid .sql backup file.');
        }

        if (strtolower($file->getClientExtension()) !== 'sql') {
            return redirect()->to('backup')->with('error', 'File must be a .sql backup.');
        }

        $sql = file_get_contents($file->getTempName());

        if ($sql === false || trim($sql) === '') {
            return redirect()->to('backup')->with('error', 'Uploaded file is empty or unreadable.');
        }

        $db     = service('tenantContext')->getConnection();
        $result = $this->runRestore($db, $sql);

        $this->logActivity('backup', $result['success']
            ? 'Restored the tenant database from an uploaded backup file (' . $result['statementCount'] . ' statements executed).'
            : 'Attempted a database restore that FAILED after ' . $result['statementCount'] . ' statements: ' . $result['error']);

        return redirect()->to('backup')->with(
            $result['success'] ? 'success' : 'error',
            $result['success'] ? 'Database restored (' . $result['statementCount'] . ' statements executed).' : 'Restore failed: ' . $result['error']
        );
    }

    /**
     * Not fully transactional, and honestly documented as such: MySQL DDL
     * (DROP/CREATE TABLE) auto-commits regardless of any transaction
     * wrapper, so a failure partway through cannot be automatically
     * rolled back. Wrapping this loop in transStart()/transComplete()
     * would only protect the row-reload portion, and even that
     * protection disappears the moment a later DROP TABLE runs. The UI
     * says this plainly before the user confirms.
     *
     * @return array{success: bool, statementCount: int, error: ?string}
     */
    private function runRestore($db, string $sql): array
    {
        $statements = $this->splitStatements($sql);
        $executed   = 0;

        $db->disableForeignKeyChecks();

        try {
            foreach ($statements as $statement) {
                $statement = trim($statement);

                if ($statement === '' || str_starts_with($statement, '--')) {
                    continue;
                }

                $db->query($statement);
                $executed++;
            }

            $db->enableForeignKeyChecks();

            return ['success' => true, 'statementCount' => $executed, 'error' => null];
        } catch (Throwable $e) {
            $db->enableForeignKeyChecks();

            return ['success' => false, 'statementCount' => $executed, 'error' => $e->getMessage()];
        }
    }

    /**
     * Splits on `;` at end of line — adequate because this only ever
     * consumes files this same feature produced (values are wrapped and
     * quoted by $db->escape(), which handles embedded semicolons/quotes
     * inside string literals correctly; a semicolon only ever terminates
     * a statement at the point our own dump() put one). A fully
     * quote-aware SQL tokenizer would be more robust but isn't needed for
     * a v1 that only round-trips its own output.
     *
     * @return string[]
     */
    private function splitStatements(string $sql): array
    {
        return preg_split('/;\r?\n/', $sql) ?: [];
    }
}
