<?php

declare(strict_types=1);

namespace PpPhp\Core;

/**
 * Platform-level settings for the PPphp engine itself (email server, SMS, and
 * future integrations).
 *
 * These are CORE settings — they configure how PPphp talks to the outside world
 * for things like account emails and one-time codes. They are deliberately
 * separate from a project's own Misc → Email/SMS settings, which are compiled
 * into a generated app; nothing here reaches the build engine or generated apps.
 *
 * Values are stored as JSON in the existing central `settings` key/value table.
 */
class PlatformSettings
{
    public const EMAIL     = 'platform.email';
    public const TEMPLATES = 'platform.email_templates';
    public const SMS       = 'platform.sms';
    public const VERSION   = 'platform.version';
    public const BRAND     = 'platform.brand';

    /** The default product name when the Owner hasn't set a custom one. */
    public const DEFAULT_BRAND_NAME    = 'PPPHP';
    public const DEFAULT_BRAND_TAGLINE = 'PHP Rapid Application Builder';

    /**
     * The Owner-configurable brand identity (name, optional logo, tagline), applied
     * across the whole platform via the \PpPhp\Core\Brand helper. `logo` is stored
     * as a self-contained data-URI so it needs no file management and travels with
     * a settings export. An empty `logo` keeps the built-in crystal PPPHP logo.
     */
    public static function brand(): array
    {
        $b = self::get(self::BRAND);
        $name = trim((string) ($b['name'] ?? ''));
        return [
            'name'    => $name !== '' ? $name : self::DEFAULT_BRAND_NAME,
            'tagline' => trim((string) ($b['tagline'] ?? self::DEFAULT_BRAND_TAGLINE)),
            'logo'    => (string) ($b['logo'] ?? ''),   // data-URI, or '' for the built-in logo
        ];
    }

    /** The configured product name (defaults to PPPHP). */
    public static function brandName(): string
    {
        return self::brand()['name'];
    }

    /**
     * The platform version shown everywhere (sidebar footer, Projects badge, …).
     *
     * SINGLE SOURCE OF TRUTH: the platform code constant Version::PLATFORM, which
     * the OTA publisher stamps forward on every release (Version::stampCode). This
     * used to read a separate `platform.version` settings row, which drifted away
     * from the App Bundle / updater once a publish bumped the code but not the row.
     * Returning the constant keeps every surface identical and auto-current.
     */
    public static function version(): string
    {
        return Version::PLATFORM;
    }

    /**
     * Kept for backward-compatibility (older update/publish paths call it). It
     * still records the value in `platform.version`, but that row is no longer
     * read for display — version() is the constant. Harmless to call.
     */
    public static function setVersion(string $value): bool
    {
        return self::set(self::VERSION, ['value' => trim($value)]);
    }

    /** Where owner notifications land until someone sets an owner in Settings → Email. */
    public const DEFAULT_OWNER_EMAIL = 'ppradhan@live.com';

    /** Per-request memo of decoded bags. The `settings` table is read constantly
     *  across the whole app (auth, theme, analytics, licensing…), so caching a key
     *  for the life of ONE request removes a stream of identical SELECTs. `null`
     *  caches a confirmed-missing key. Kept fresh by set(). */
    private static array $memo = [];

    /** Read a settings bag. Returns $default when unset or unreadable. */
    public static function get(string $key, array $default = []): array
    {
        if (array_key_exists($key, self::$memo)) {
            $c = self::$memo[$key];
            return $c === null ? $default : $c + $default;
        }
        try {
            DB::Init();
            DB::SetConnection();
            $rows = DB::FetchAll('SELECT `value` FROM `settings` WHERE `key` = ?', [$key]);
            $raw = $rows[0]['value'] ?? '';
            if (!is_string($raw) || $raw === '') {
                self::$memo[$key] = null;
                return $default;
            }
            $data = json_decode($raw, true);
            if (is_array($data)) {
                self::$memo[$key] = $data;
                return $data + $default;
            }
            self::$memo[$key] = null;
            return $default;
        } catch (\Throwable $e) {
            return $default;   // transient error — don't poison the memo
        }
    }

    /** Write a settings bag. Returns true on success. */
    public static function set(string $key, array $value): bool
    {
        try {
            DB::Init();
            DB::SetConnection();
            $json = (string) json_encode($value, JSON_UNESCAPED_SLASHES);
            $ok = DB::Query(
                'INSERT INTO `settings` (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)',
                [$key, $json]
            );
            if ($ok !== false) self::$memo[$key] = $value;   // keep the per-request memo consistent
            return $ok !== false;
        } catch (\Throwable $e) {
            return false;
        }
    }

    /** True once an email server is configured well enough to send. */
    public static function emailReady(): bool
    {
        $e = self::get(self::EMAIL);
        // OAuth-connected mailbox: the host/username come from the provider, so
        // "ready" means the account is connected (a refresh token is stored).
        if (strtolower((string) ($e['authType'] ?? '')) === 'xoauth2'
            && \PpPhp\Core\Mail\MailOAuth::isProvider((string) ($e['oauthProvider'] ?? ''))
            && trim((string) ($e['oauthRefreshToken'] ?? '')) !== '') {
            return true;
        }
        return trim((string) ($e['host'] ?? '')) !== ''
            && trim((string) ($e['fromAddress'] ?? '')) !== '';
    }

    /**
     * The effective PRIMARY mail config for sending. For a password/API-key setup this
     * is the saved bag as-is; for an OAuth-connected mailbox it resolves the stored
     * refresh token into a short-lived access token and fills host/port/username from
     * the provider, so the SMTP engine can authenticate via AUTH XOAUTH2.
     */
    public static function resolveMailPrimary(): array
    {
        $e = self::get(self::EMAIL);
        return self::resolveMailServer($e, $e);
    }

    /**
     * Resolve ONE server config (primary OR a fallback) for the SMTP engine. For an
     * OAuth mailbox (authType=xoauth2) it mints a fresh access token and fills host/
     * port/username from the provider so the engine authenticates via AUTH XOAUTH2;
     * otherwise the basic host/user/password config is returned. `$shared` supplies
     * the From identity + app name when a server leaves them blank.
     */
    public static function resolveMailServer(array $cfg, array $shared = []): array
    {
        $sharedFrom = trim((string) ($shared['fromAddress'] ?? ''));
        $sharedName = (string) ($shared['fromName'] ?? '');
        $appName    = (string) ($shared['appName'] ?? 'PPphp');

        if (strtolower((string) ($cfg['authType'] ?? '')) === 'xoauth2'
            && \PpPhp\Core\Mail\MailOAuth::isProvider((string) ($cfg['oauthProvider'] ?? ''))) {
            $provider = (string) $cfg['oauthProvider'];
            $token = \PpPhp\Core\Mail\MailOAuth::accessToken(
                $provider,
                (string) ($cfg['oauthClientId'] ?? ''),
                self::secretDecrypt((string) ($cfg['oauthClientSecret'] ?? '')),
                self::secretDecrypt((string) ($cfg['oauthRefreshToken'] ?? ''))
            );
            $smtp = \PpPhp\Core\Mail\MailOAuth::smtp($provider);
            $addr = (string) ($cfg['oauthEmail'] ?? '') ?: (trim((string) ($cfg['fromAddress'] ?? '')) ?: $sharedFrom);
            return [
                'host'        => (string) ($smtp['host'] ?? ''),
                'port'        => (string) ($smtp['port'] ?? 587),
                'encryption'  => (string) ($smtp['encryption'] ?? 'tls'),
                'username'    => $addr,
                'authType'    => 'xoauth2',
                'oauthToken'  => $token,
                'fromAddress' => trim((string) ($cfg['fromAddress'] ?? '')) ?: $addr,
                'fromName'    => (string) ($cfg['fromName'] ?? '') ?: $sharedName,
                'appName'     => $appName,
            ];
        }

        return [
            'host'        => trim((string) ($cfg['host'] ?? '')),
            'port'        => (string) ($cfg['port'] ?? '587'),
            'encryption'  => (string) ($cfg['encryption'] ?? 'tls'),
            'username'    => trim((string) ($cfg['username'] ?? '')),
            'password'    => (string) ($cfg['password'] ?? ''),
            'fromAddress' => trim((string) ($cfg['fromAddress'] ?? '')) ?: $sharedFrom,
            'fromName'    => trim((string) ($cfg['fromName'] ?? '')) ?: $sharedName,
            'appName'     => $appName,
        ];
    }

    /** Encrypt a secret for storage (FieldCrypto; base64 fallback marked "b64:"). */
    public static function secretEncrypt(string $plain): string
    {
        if ($plain === '') return '';
        if (class_exists('\PpPhp\Core\Auth\FieldCrypto')) {
            try { return \PpPhp\Core\Auth\FieldCrypto::encrypt($plain); } catch (\Throwable) {}
        }
        return 'b64:' . base64_encode($plain);
    }

    /** Reverse of secretEncrypt(). */
    public static function secretDecrypt(string $stored): string
    {
        if ($stored === '') return '';
        if (str_starts_with($stored, 'b64:')) return (string) base64_decode(substr($stored, 4));
        if (class_exists('\PpPhp\Core\Auth\FieldCrypto')) {
            try { return \PpPhp\Core\Auth\FieldCrypto::decrypt($stored); } catch (\Throwable) {}
        }
        return $stored;
    }

    /**
     * The address that receives owner-bound notifications (e.g. someone asking
     * for trial access). Falls back to DEFAULT_OWNER_EMAIL rather than the From
     * address: From is a sending identity and is often an unmonitored no-reply,
     * so owner mail sent there can go unread.
     */
    public static function ownerEmail(): string
    {
        $e = self::get(self::EMAIL);
        $owner = trim((string) ($e['ownerEmail'] ?? ''));

        return $owner !== '' ? $owner : self::DEFAULT_OWNER_EMAIL;
    }

    /**
     * Load the configured email server into the Email core, ready to send.
     * Returns false when email has not been configured yet.
     */
    public static function primeMailer(): bool
    {
        // The customer's OWN configured email always wins.
        if (self::emailReady()) {
            Email::setSettings(self::resolveMailPrimary());   // OAuth → mints an access token
            Email::setFallbacks(self::mailFallbacks());       // resilience: try #2, #3 if #1 fails
            return true;
        }
        // Otherwise fall back to the email config baked into the bundle, so a
        // fresh/updated install can still send (owner recovery, account emails)
        // with nothing configured. Empty on an unstamped development build.
        if (class_exists('\PpPhp\Core\MailDefault') && \PpPhp\Core\MailDefault::isConfigured()) {
            Email::setSettings(\PpPhp\Core\MailDefault::config());
            Email::setFallbacks([]);
            return true;
        }
        return false;
    }

    /**
     * The ordered fallback mail servers (only those with a host), each normalised
     * and given the primary's From identity / app name when its own is blank — so a
     * fallback entry that only carries host+credentials still sends valid mail.
     * The primary server is NOT included here (it is loaded via setSettings()).
     *
     * @return array<int,array<string,string>>
     */
    public static function mailFallbacks(): array
    {
        $e   = self::get(self::EMAIL);
        $out = [];
        foreach ((array) ($e['fallbacks'] ?? []) as $fb) {
            if (!is_array($fb)) continue;
            // A fallback is usable if it has an SMTP host (basic) OR a connected OAuth grant.
            $isOauth = strtolower((string) ($fb['authType'] ?? '')) === 'xoauth2'
                && \PpPhp\Core\Mail\MailOAuth::isProvider((string) ($fb['oauthProvider'] ?? ''))
                && trim((string) ($fb['oauthRefreshToken'] ?? '')) !== '';
            if (!$isOauth && trim((string) ($fb['host'] ?? '')) === '') continue;
            $out[] = self::resolveMailServer($fb, $e);   // resolves OAuth → access token when needed
            if (count($out) >= 2) break;                 // up to two fallbacks (three servers total)
        }
        return $out;
    }

    /**
     * The built-in account emails.
     *
     * Bodies are HTML fragments — Mailer wraps them in the branded shell, so a
     * template only needs to describe its own content. Inline styles only:
     * email clients ignore <style> blocks and CSS variables.
     */
    public static function defaultTemplates(): array
    {
        $muted  = 'color:#64748b;font-size:12.5px;line-height:1.6;';
        $btn    = 'display:inline-block;background:#4f46e5;color:#ffffff;text-decoration:none;font-weight:600;font-size:14px;padding:11px 22px;border-radius:8px;';
        $h      = 'margin:0 0 12px;font-size:18px;font-weight:700;color:#0f172a;';
        $p      = 'margin:0 0 14px;';
        $code   = 'display:inline-block;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:26px;font-weight:700;letter-spacing:6px;color:#4f46e5;background:#eef2ff;border:1px solid #c7d2fe;border-radius:10px;padding:12px 20px;';
        $panel  = 'background:#f8fafc;border:1px solid #eef1f6;border-radius:10px;padding:14px 16px;margin:0 0 16px;';

        return [
            'welcome' => [
                'label'   => 'Welcome message',
                'hint'    => 'Sent when an administrator creates a new account.',
                'subject' => 'Welcome to {app_name}, {username}!',
                'body'    => "<h1 style=\"{$h}\">Welcome aboard 👋</h1>\n"
                    . "<p style=\"{$p}\">Hi <strong>{username}</strong>, your {app_name} account is ready to use.</p>\n"
                    . "<div style=\"{$panel}\">\n"
                    . "  <div style=\"{$muted}\">Your username</div>\n"
                    . "  <div style=\"font-size:15px;font-weight:700;color:#0f172a;\">{username}</div>\n"
                    . "</div>\n"
                    . "<p style=\"{$p}\"><a href=\"{login_url}\" style=\"{$btn}\">Sign in to {app_name}</a></p>\n"
                    . "<p style=\"{$muted}\">Your password was set by the administrator who created this account — ask them if you don't have it yet. You can change it any time from your profile.</p>",
            ],
            'forgot_password' => [
                'label'   => 'Forgot password',
                'hint'    => 'Sent when someone asks to reset their password. Includes the code and reset link.',
                'subject' => 'Reset your {app_name} password',
                'body'    => "<h1 style=\"{$h}\">Reset your password</h1>\n"
                    . "<p style=\"{$p}\">Hi <strong>{username}</strong>, we got a request to reset the password for your {app_name} account.</p>\n"
                    . "<p style=\"{$p};text-align:center;\"><span style=\"{$code}\">{otp}</span></p>\n"
                    . "<p style=\"{$p};text-align:center;\"><a href=\"{reset_url}\" style=\"{$btn}\">Choose a new password</a></p>\n"
                    . "<p style=\"{$muted}\">This code and link expire in <strong>{expires_in}</strong>. If you didn't ask for this, you can safely ignore this email — your password hasn't changed.</p>",
            ],
            'password_changed' => [
                'label'   => 'Password changed',
                'hint'    => 'Confirmation sent after a password is changed.',
                'subject' => 'Your {app_name} password was changed',
                'body'    => "<h1 style=\"{$h}\">Your password was changed</h1>\n"
                    . "<p style=\"{$p}\">Hi <strong>{username}</strong>, the password for your {app_name} account was just changed.</p>\n"
                    . "<div style=\"{$panel}\">\n"
                    . "  <div style=\"{$muted}\">Account</div>\n"
                    . "  <div style=\"font-size:14px;font-weight:600;color:#0f172a;\">{email}</div>\n"
                    . "</div>\n"
                    . "<p style=\"{$p}\">If this was you, there's nothing to do.</p>\n"
                    . "<p style=\"{$muted}\">If it <strong>wasn't</strong> you, please contact your administrator straight away — someone else may have access to your account.</p>",
            ],
            'role_changed' => [
                'label'   => 'Access level changed',
                'hint'    => 'Sent when an administrator changes what an account can do.',
                'subject' => 'Your {app_name} access level changed',
                'body'    => "<h1 style=\"{$h}\">Your access level changed</h1>\n"
                    . "<p style=\"{$p}\">Hi <strong>{username}</strong>, an administrator updated what your {app_name} account can do.</p>\n"
                    . "<div style=\"{$panel}\">\n"
                    . "  <div style=\"{$muted}\">You are now</div>\n"
                    . "  <div style=\"font-size:15px;font-weight:700;color:#0f172a;text-transform:capitalize;\">{role}</div>\n"
                    . "</div>\n"
                    . "<p style=\"{$p}\"><a href=\"{login_url}\" style=\"{$btn}\">Open {app_name}</a></p>\n"
                    . "<p style=\"{$muted}\">If this looks wrong, please contact your administrator.</p>",
            ],
            'account_deleted' => [
                'label'   => 'Account removed',
                'hint'    => 'Sent when an administrator deletes an account.',
                'subject' => 'Your {app_name} account was removed',
                'body'    => "<h1 style=\"{$h}\">Your account was removed</h1>\n"
                    . "<p style=\"{$p}\">Hi <strong>{username}</strong>, your {app_name} account has been removed by an administrator and you can no longer sign in.</p>\n"
                    . "<p style=\"{$muted}\">If you think this was a mistake, please get in touch with your administrator.</p>",
            ],
            'access_request' => [
                'label'   => 'Trial access request',
                'hint'    => 'Sent to the owner when someone asks to try {app_name}. The requester is copied in.',
                'to'      => 'owner',
                'vars'    => ['{app_name}', '{full_name}', '{email}', '{organization}', '{role_title}', '{use_case}', '{duration}', '{request_id}', '{requested_at}', '{review_url}'],
                'subject' => 'New trial access request from {full_name} ({duration})',
                'body'    => "<h1 style=\"{$h}\">Someone wants to try {app_name} 🎉</h1>\n"
                    . "<p style=\"{$p}\"><strong>{full_name}</strong> asked for <strong>{duration}</strong> of trial access.</p>\n"
                    . "<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"{$panel}\">\n"
                    . "  <tr><td style=\"{$muted};width:110px;padding:5px 0;\">Name</td><td style=\"padding:5px 0;font-weight:600;color:#0f172a;\">{full_name}</td></tr>\n"
                    . "  <tr><td style=\"{$muted};padding:5px 0;\">Email</td><td style=\"padding:5px 0;font-weight:600;color:#0f172a;\"><a href=\"mailto:{email}\" style=\"color:#4f46e5;text-decoration:none;\">{email}</a></td></tr>\n"
                    . "  <tr><td style=\"{$muted};padding:5px 0;\">Organization</td><td style=\"padding:5px 0;color:#0f172a;\">{organization}</td></tr>\n"
                    . "  <tr><td style=\"{$muted};padding:5px 0;\">Role</td><td style=\"padding:5px 0;color:#0f172a;\">{role_title}</td></tr>\n"
                    . "  <tr><td style=\"{$muted};padding:5px 0;\">Wants</td><td style=\"padding:5px 0;font-weight:600;color:#0f172a;\">{duration}</td></tr>\n"
                    . "  <tr><td style=\"{$muted};padding:5px 0;\">Requested</td><td style=\"padding:5px 0;color:#0f172a;\">{requested_at}</td></tr>\n"
                    . "</table>\n"
                    . "<p style=\"margin:0 0 6px;font-weight:700;color:#0f172a;\">What they want to build</p>\n"
                    . "<p style=\"{$p};padding:12px 14px;background:#f8fafc;border-left:3px solid #4f46e5;border-radius:0 8px 8px 0;\">{use_case}</p>\n"
                    . "<p style=\"{$p}\"><a href=\"{review_url}\" style=\"{$btn}\">Review this request</a></p>\n"
                    . "<p style=\"{$muted}\">Request #{request_id} — {full_name} is copied on this email, so you can simply reply to reach them.</p>",
            ],
        ];
    }

    /** Templates merged over the defaults, so new built-ins appear automatically. */
    public static function templates(): array
    {
        $saved = self::get(self::TEMPLATES);
        $out = [];
        foreach (self::defaultTemplates() as $key => $def) {
            $out[$key] = [
                'label'   => $def['label'],
                'hint'    => $def['hint'],
                // Who it goes to: the account holder, or the owner address.
                'to'      => (string) ($def['to'] ?? 'user'),
                // Placeholders that actually mean something for THIS template.
                'vars'    => (array) ($def['vars'] ?? self::placeholders()),
                'subject' => (string) ($saved[$key]['subject'] ?? $def['subject']),
                'body'    => (string) ($saved[$key]['body'] ?? $def['body']),
                // Templates are on by default; an admin can switch one off so its
                // email is never sent (previews still render).
                'enabled' => !isset($saved[$key]['enabled']) || (bool) $saved[$key]['enabled'],
                // Genuinely edited = saved content that differs from the shipped default
                // (a plain "Save all" that rewrites identical content is NOT "edited").
                'customized' => (isset($saved[$key]['subject']) && (string) $saved[$key]['subject'] !== $def['subject'])
                    || (isset($saved[$key]['body']) && (string) $saved[$key]['body'] !== $def['body']),
            ];
        }
        return $out;
    }

    /** True when a template is switched on (so its email should actually send). */
    public static function templateEnabled(string $key): bool
    {
        $saved = self::get(self::TEMPLATES);
        // Unknown keys and never-touched templates default to ON.
        return !isset($saved[$key]['enabled']) || (bool) $saved[$key]['enabled'];
    }

    /** Placeholders available to the account templates. */
    public static function placeholders(): array
    {
        return ['{app_name}', '{username}', '{email}', '{role}', '{otp}', '{reset_url}', '{login_url}', '{expires_in}'];
    }

    /** Fill {placeholders} in a template string. */
    public static function render(string $text, array $vars): string
    {
        foreach ($vars as $k => $v) {
            $text = str_replace('{' . $k . '}', (string) $v, $text);
        }
        return $text;
    }
}
