<?php

declare(strict_types=1);

namespace PpPhp\Core\Auth;

/**
 * Transactional email templates for a generated app's account lifecycle.
 *
 * One catalogue, used by three callers:
 *   • the console UI  — lists, edits, previews and resets them
 *   • the build       — bakes the project's copies into the generated app
 *   • the app itself  — renders and sends them
 *
 * Keeping the defaults here (rather than in the UI or the build) is what stops
 * the three drifting apart, which is exactly how earlier settings pages ended
 * up describing behaviour the app never had.
 *
 * Every template is stored as {subject, body, enabled}. `body` is the INNER
 * html — the branded shell around it is shared, so a developer editing one
 * template cannot break the layout of the others, and a redesign of the shell
 * updates every email at once.
 */
final class AuthEmails
{
    /** Placeholders every template may use. */
    public static function commonVars(): array
    {
        return ['{app_name}', '{username}', '{email}', '{fullname}', '{login_url}', '{year}'];
    }

    /**
     * The catalogue. `to` records who receives it, which the UI shows so nobody
     * has to guess whether a template reaches the user or the administrator.
     *
     * @return array<string, array{label:string, hint:string, to:string, vars:list<string>, subject:string, body:string}>
     */
    public static function defaults(): array
    {
        return [
            'verify_email' => [
                'label'   => 'Verify email address',
                'hint'    => 'Sent when a new account needs to confirm its email before signing in.',
                'to'      => 'user',
                'vars'    => ['{verify_url}', '{expires_in}'],
                'subject' => 'Confirm your {app_name} account',
                'body'    => self::lead('Confirm your email')
                    . self::para('Hi {fullname}, thanks for creating an account on <strong>{app_name}</strong>. Please confirm this email address to activate your account.')
                    . self::button('Confirm my email', '{verify_url}')
                    . self::note('This link expires in {expires_in}. If you did not create this account you can safely ignore this email.')
                    . self::fallback('{verify_url}'),
            ],
            'welcome' => [
                'label'   => 'Welcome',
                'hint'    => 'Sent once an account becomes active and can sign in.',
                'to'      => 'user',
                'vars'    => [],
                'subject' => 'Welcome to {app_name}',
                'body'    => self::lead('You are all set')
                    . self::para('Hi {fullname}, your <strong>{app_name}</strong> account is active. You can sign in any time with the username <strong>{username}</strong>.')
                    . self::button('Go to {app_name}', '{login_url}')
                    . self::note('Keep your password to yourself — nobody from {app_name} will ever ask you for it.'),
            ],
            'awaiting_approval' => [
                'label'   => 'Awaiting approval',
                'hint'    => 'Sent to the applicant when an administrator must approve the account.',
                'to'      => 'user',
                'vars'    => [],
                'subject' => 'Your {app_name} account is awaiting approval',
                'body'    => self::lead('Thanks for signing up')
                    . self::para('Hi {fullname}, your account on <strong>{app_name}</strong> has been created and is waiting for an administrator to approve it.')
                    . self::para('We will email you as soon as it is approved — there is nothing further for you to do right now.')
                    . self::note('Signed up as {username} ({email}).'),
            ],
            'account_approved' => [
                'label'   => 'Account approved',
                'hint'    => 'Sent when an administrator approves a pending account.',
                'to'      => 'user',
                'vars'    => [],
                'subject' => 'Your {app_name} account has been approved',
                'body'    => self::lead('Your account is ready')
                    . self::para('Good news {fullname} — your <strong>{app_name}</strong> account has been approved and you can sign in now.')
                    . self::button('Sign in', '{login_url}')
                    . self::note('You signed up as {username}.'),
            ],
            'account_rejected' => [
                'label'   => 'Account declined',
                'hint'    => 'Sent when an administrator declines a pending account.',
                'to'      => 'user',
                'vars'    => ['{reason}'],
                'subject' => 'About your {app_name} account request',
                'body'    => self::lead('We could not approve this request')
                    . self::para('Hi {fullname}, unfortunately your request for access to <strong>{app_name}</strong> was not approved.')
                    . self::quote('{reason}')
                    . self::note('If you think this was a mistake, reply to this email and we will take another look.'),
            ],
            'admin_new_signup' => [
                'label'   => 'New sign-up (administrator)',
                'hint'    => 'Tells the administrator that somebody is waiting for approval.',
                'to'      => 'admin',
                'vars'    => ['{review_url}', '{signup_ip}', '{signup_at}'],
                'subject' => 'New {app_name} sign-up awaiting approval',
                'body'    => self::lead('Someone is waiting for approval')
                    . self::para('A new account has been created on <strong>{app_name}</strong> and needs your decision.')
                    . self::details([
                        'Name'     => '{fullname}',
                        'Username' => '{username}',
                        'Email'    => '{email}',
                        'Signed up'=> '{signup_at}',
                        'From'     => '{signup_ip}',
                    ])
                    . self::button('Review this account', '{review_url}'),
            ],
            'reset_password' => [
                'label'   => 'Password reset',
                'hint'    => 'The forgot-password link.',
                'to'      => 'user',
                'vars'    => ['{reset_url}', '{expires_in}'],
                'subject' => 'Reset your {app_name} password',
                'body'    => self::lead('Reset your password')
                    . self::para('Hi {fullname}, we received a request to reset the password for <strong>{username}</strong> on {app_name}.')
                    . self::button('Choose a new password', '{reset_url}')
                    . self::note('This link expires in {expires_in} and can only be used once. If you did not ask for this, ignore this email — your password will not change.')
                    . self::fallback('{reset_url}'),
            ],
            'password_changed' => [
                'label'   => 'Password changed',
                'hint'    => 'Confirms a password change, so an unexpected one gets noticed.',
                'to'      => 'user',
                'vars'    => ['{changed_at}', '{signup_ip}'],
                'subject' => 'Your {app_name} password was changed',
                'body'    => self::lead('Your password was changed')
                    . self::para('Hi {fullname}, the password for <strong>{username}</strong> on {app_name} was changed on {changed_at} from {signup_ip}.')
                    . self::note('If this was you, no action is needed. If it was not, contact an administrator immediately — your account may be compromised.')
                    . self::button('Sign in', '{login_url}'),
            ],
            'account_locked' => [
                'label'   => 'Account locked',
                'hint'    => 'Sent when repeated failed sign-ins lock an account.',
                'to'      => 'user',
                'vars'    => ['{lock_minutes}', '{signup_ip}'],
                'subject' => 'Your {app_name} account was temporarily locked',
                'body'    => self::lead('Too many failed sign-ins')
                    . self::para('Hi {fullname}, the account <strong>{username}</strong> was locked after several failed sign-in attempts from {signup_ip}.')
                    . self::note('It unlocks automatically in {lock_minutes} minutes. If this was not you, someone may be trying to guess your password — consider changing it once you are back in.'),
            ],
        ];
    }

    // ── building blocks ─────────────────────────────────────────────────
    // Inline styles only: email clients strip <style> blocks and ignore
    // classes, so anything that must survive Outlook has to be inline.
    private static function lead(string $text): string
    {
        return '<h1 style="margin:0 0 14px;font-size:22px;line-height:1.3;font-weight:800;color:#0f172a;">' . $text . '</h1>';
    }

    private static function para(string $html): string
    {
        return '<p style="margin:0 0 14px;font-size:15px;line-height:1.65;color:#334155;">' . $html . '</p>';
    }

    private static function note(string $html): string
    {
        return '<p style="margin:18px 0 0;font-size:13px;line-height:1.6;color:#64748b;">' . $html . '</p>';
    }

    private static function quote(string $html): string
    {
        return '<p style="margin:0 0 14px;padding:12px 14px;background:#f8fafc;border-left:3px solid #cbd5e1;'
            . 'border-radius:0 8px 8px 0;font-size:14px;line-height:1.6;color:#334155;">' . $html . '</p>';
    }

    private static function button(string $label, string $url): string
    {
        return '<p style="margin:22px 0;"><a href="' . $url . '" style="display:inline-block;padding:12px 22px;'
            . 'background:#4f46e5;color:#ffffff;font-size:15px;font-weight:700;text-decoration:none;'
            . 'border-radius:10px;">' . $label . '</a></p>';
    }

    /** Because some clients strip buttons, always offer the raw URL too. */
    private static function fallback(string $url): string
    {
        return '<p style="margin:14px 0 0;font-size:12px;line-height:1.6;color:#94a3b8;word-break:break-all;">'
            . 'If the button does not work, paste this into your browser:<br>' . $url . '</p>';
    }

    /** @param array<string,string> $rows */
    private static function details(array $rows): string
    {
        $html = '<table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;margin:0 0 16px;'
            . 'border-collapse:collapse;font-size:14px;">';
        foreach ($rows as $k => $v) {
            $html .= '<tr>'
                . '<td style="padding:6px 0;color:#64748b;width:38%;">' . $k . '</td>'
                . '<td style="padding:6px 0;color:#0f172a;font-weight:600;">' . $v . '</td>'
                . '</tr>';
        }
        return $html . '</table>';
    }

    /**
     * Wrap a template body in the shared branded shell.
     *
     * Table-based and inline-styled on purpose: this is the layout that still
     * renders correctly in Outlook, which ignores modern CSS layout entirely.
     */
    public static function shell(string $innerHtml, string $appName, string $accent = '#4f46e5'): string
    {
        $year = date('Y');
        $safeName = htmlspecialchars($appName, ENT_QUOTES, 'UTF-8');
        $initials = htmlspecialchars(mb_strtoupper(mb_substr($appName, 0, 2)), ENT_QUOTES, 'UTF-8');
        return '<!DOCTYPE html><html><head><meta charset="utf-8">'
            . '<meta name="viewport" content="width=device-width,initial-scale=1">'
            . '<title>' . $safeName . '</title></head>'
            . '<body style="margin:0;padding:0;background:#eef2f7;">'
            . '<table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;background:#eef2f7;padding:28px 12px;">'
            . '<tr><td align="center">'
            . '<table role="presentation" cellpadding="0" cellspacing="0" style="width:100%;max-width:560px;background:#ffffff;'
            . 'border-radius:16px;overflow:hidden;box-shadow:0 6px 24px rgba(15,23,42,.08);'
            . 'font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,Helvetica,Arial,sans-serif;">'
            // header
            . '<tr><td style="padding:22px 28px;background:' . $accent . ';">'
            . '<table role="presentation" cellpadding="0" cellspacing="0"><tr>'
            . '<td style="width:38px;height:38px;background:rgba(255,255,255,.22);border-radius:10px;text-align:center;'
            . 'vertical-align:middle;color:#ffffff;font-weight:800;font-size:15px;">' . $initials . '</td>'
            . '<td style="padding-left:12px;color:#ffffff;font-size:17px;font-weight:800;">' . $safeName . '</td>'
            . '</tr></table></td></tr>'
            // body
            . '<tr><td style="padding:28px;">' . $innerHtml . '</td></tr>'
            // footer
            . '<tr><td style="padding:16px 28px 24px;border-top:1px solid #e8edf4;">'
            . '<p style="margin:0;font-size:12px;line-height:1.6;color:#94a3b8;">'
            . 'This is an automated message from ' . $safeName . '. Please do not reply.<br>'
            . '&copy; ' . $year . ' ' . $safeName . '</p>'
            . '</td></tr>'
            . '</table></td></tr></table></body></html>';
    }

    /**
     * Templates merged over the defaults, so a template added in a later
     * release shows up automatically instead of silently missing.
     *
     * @param array<string, array> $saved
     */
    public static function all(array $saved = []): array
    {
        $out = [];
        foreach (self::defaults() as $key => $def) {
            $s = (array) ($saved[$key] ?? []);
            $out[$key] = [
                'label'   => $def['label'],
                'hint'    => $def['hint'],
                'to'      => $def['to'],
                'vars'    => array_values(array_unique(array_merge(self::commonVars(), $def['vars']))),
                'subject' => (string) ($s['subject'] ?? $def['subject']),
                'body'    => (string) ($s['body'] ?? $def['body']),
                'enabled' => !isset($s['enabled']) || (bool) $s['enabled'],
                // "Edited" means the stored copy DIFFERS from the shipped default;
                // re-saving identical content must not count as a customisation.
                'customized' => (isset($s['subject']) && (string) $s['subject'] !== $def['subject'])
                    || (isset($s['body']) && (string) $s['body'] !== $def['body']),
            ];
        }
        return $out;
    }

    public static function isEnabled(string $key, array $saved = []): bool
    {
        $s = (array) ($saved[$key] ?? []);
        return !isset($s['enabled']) || (bool) $s['enabled'];
    }

    /** Replace {placeholders}. Unknown ones are left alone, never blanked. */
    public static function render(string $text, array $vars): string
    {
        foreach ($vars as $k => $v) {
            $text = str_replace('{' . $k . '}', (string) $v, $text);
        }
        return $text;
    }

    /**
     * Full, ready-to-send email for one template.
     *
     * @return array{subject:string, html:string, enabled:bool}
     */
    public static function compose(string $key, array $vars, array $saved = [], string $accent = '#4f46e5'): array
    {
        $all = self::all($saved);
        $tpl = $all[$key] ?? null;
        if ($tpl === null) {
            return ['subject' => '', 'html' => '', 'enabled' => false];
        }
        $appName = (string) ($vars['app_name'] ?? 'App');
        $vars += ['year' => date('Y')];
        return [
            'subject' => self::render($tpl['subject'], $vars),
            'html'    => self::shell(self::render($tpl['body'], $vars), $appName, $accent),
            'enabled' => (bool) $tpl['enabled'],
        ];
    }

    /** Believable sample values so a preview looks like a real email. */
    public static function sampleVars(string $appName = 'Acme Operations'): array
    {
        return [
            'app_name'     => $appName,
            'username'     => 'jordan.lee',
            'fullname'     => 'Jordan Lee',
            'email'        => 'jordan.lee@example.com',
            'login_url'    => 'https://example.com/generated/auth/login.php',
            'verify_url'   => 'https://example.com/generated/auth/verify.php?token=5f3a…',
            'reset_url'    => 'https://example.com/generated/auth/reset-password.php?token=9c1b…',
            'review_url'   => 'https://example.com/generated/users/list.php',
            'expires_in'   => '24 hours',
            'reason'       => 'We could not match your details to an existing customer record.',
            'changed_at'   => date('j M Y, H:i'),
            'signup_at'    => date('j M Y, H:i'),
            'signup_ip'    => '203.0.113.42',
            'lock_minutes' => '15',
            'year'         => date('Y'),
        ];
    }
}
