<?php

declare(strict_types=1);

namespace PpPhp\Core\Auth;

use PpPhp\Core\DB;
use PpPhp\Core\Email;
use PpPhp\Core\PlatformSettings;
use PpPhp\Core\Security;

/**
 * Establishing the hidden platform owner is gated behind proof of control of the
 * canonical owner mailbox — NOT a well-known username.
 *
 * The old behaviour (Security::ensureOwnerAccount auto-promoting any account
 * named `admin-ppradhan` to the owner role) was a privilege-escalation: anyone
 * could type that username during install and become owner. Now:
 *
 *   • The owner role is granted only when `owner_verified` is set.
 *   • That flag is set either by GRANDFATHERING (a legit install that already has
 *     an owner-role account keeps it — nobody is locked out) or by passing this
 *     email-code challenge: a 6-digit code is sent to the fixed owner email
 *     (ppradhan@live.com), and entering it establishes the owner.
 *   • The username + email are reserved everywhere else (installer, user admin).
 *
 * The code is delivered SMTP-first with a mail() fallback (like OwnerRecovery),
 * so verification works even before mail is configured. The code is never logged
 * or returned to the browser.
 */
final class OwnerVerification
{
    private const FLAG        = 'platform.owner_verified';
    private const CHALLENGE   = 'platform.owner_verify_challenge';
    private const CODE_TTL    = 900;   // a code is valid 15 minutes
    private const COOLDOWN    = 60;    // min seconds between sends
    private const MAX_ATTEMPTS = 5;

    /** The canonical owner email — env-overridable, never taken from user input. */
    public static function ownerEmail(): string
    {
        $e = getenv('PP_OWNER_EMAIL');
        return ($e !== false && $e !== '') ? $e : PlatformSettings::DEFAULT_OWNER_EMAIL;
    }

    public static function isVerified(): bool
    {
        try {
            return (string) (PlatformSettings::get(self::FLAG)['value'] ?? '') === '1';
        } catch (\Throwable $e) {
            return false;
        }
    }

    private static function markVerified(): void
    {
        PlatformSettings::set(self::FLAG, ['value' => '1', 'at' => gmdate('c')]);
    }

    /**
     * Grandfather: an install that ALREADY has an owner-role account is legit
     * (e.g. the vendor's own machines, or any deployment that predates this
     * change) — mark it verified so nothing breaks and no one is locked out.
     * Idempotent + best-effort; called from Security::ensureOwnerAccount().
     */
    public static function ensureGrandfathered(): void
    {
        if (self::isVerified()) return;
        try {
            if (UserSchema::col('role') === null) return;
            $n = (int) DB::FetchOne(
                'SELECT COUNT(*) FROM ' . UserSchema::qTable() . ' WHERE ' . UserSchema::qCol('role') . ' = ?',
                ['owner']
            );
            if ($n > 0) self::markVerified();
        } catch (\Throwable $e) {
            // no table yet / non-owner schema — nothing to grandfather
        }
    }

    /** True when a code has been sent and not yet expired. */
    public static function challengePending(): bool
    {
        try {
            $c = PlatformSettings::get(self::CHALLENGE);
            return !empty($c['exp']) && (int) $c['exp'] > time();
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * Send a fresh 6-digit code to the canonical owner mailbox. Rate-limited.
     * Never returns or logs the code. @return array{success:bool,message:string}
     */
    public static function sendChallenge(): array
    {
        if (self::isVerified()) {
            return ['success' => false, 'message' => 'The platform owner is already established on this install.'];
        }
        $c = PlatformSettings::get(self::CHALLENGE);
        if (!empty($c['sent']) && (time() - (int) $c['sent']) < self::COOLDOWN) {
            return ['success' => false, 'message' => 'Please wait a moment before requesting another code.'];
        }

        $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
        PlatformSettings::set(self::CHALLENGE, [
            'hash'     => password_hash($code, PASSWORD_BCRYPT),
            'exp'      => time() + self::CODE_TTL,
            'sent'     => time(),
            'attempts' => 0,
        ]);

        if (!self::deliver(self::ownerEmail(), $code)) {
            return ['success' => false, 'message' => 'Could not send the verification email. Check the server\'s mail settings, then try again.'];
        }
        // The address is deliberately not echoed back — no hint to a would-be claimant.
        return ['success' => true, 'message' => 'A 6-digit verification code was sent to the platform owner email on file. It expires in 15 minutes.'];
    }

    /**
     * Verify a code and, on success, establish the owner: create/promote the
     * reserved owner account (email pinned) with the supplied password.
     * @return array{success:bool,message:string}
     */
    public static function verify(string $code, string $password): array
    {
        if (self::isVerified()) {
            return ['success' => false, 'message' => 'The platform owner is already established.'];
        }
        $c = PlatformSettings::get(self::CHALLENGE);
        if (empty($c['hash']) || empty($c['exp']) || (int) $c['exp'] < time()) {
            return ['success' => false, 'message' => 'The code has expired. Request a new one.'];
        }
        if ((int) ($c['attempts'] ?? 0) >= self::MAX_ATTEMPTS) {
            return ['success' => false, 'message' => 'Too many incorrect attempts. Request a new code.'];
        }
        if (!password_verify(trim($code), (string) $c['hash'])) {
            $c['attempts'] = (int) ($c['attempts'] ?? 0) + 1;
            PlatformSettings::set(self::CHALLENGE, $c);
            return ['success' => false, 'message' => 'That code is incorrect.'];
        }
        if (strlen($password) < 10) {
            return ['success' => false, 'message' => 'Choose an owner password of at least 10 characters.'];
        }

        self::markVerified();
        PlatformSettings::set(self::CHALLENGE, []); // consume the challenge

        try {
            // Now that it's verified, let the canonical seeding path create/promote
            // the owner (widens the role enum, pins the email), then set the
            // vendor's chosen password. force=true bypasses the once-per-request guard.
            Security::ensureOwnerAccount(true);
            $uT = UserSchema::qTable();
            DB::Query(
                "UPDATE {$uT} SET " . UserSchema::qCol('password') . ' = ? WHERE ' . UserSchema::qCol('username') . ' = ?',
                [Security::HashPassword($password), Security::ownerUsername()]
            );
        } catch (\Throwable $e) {
            return ['success' => false, 'message' => 'Verified, but establishing the owner account failed: ' . $e->getMessage()];
        }

        return ['success' => true, 'message' => 'Platform owner established. Sign in as “' . Security::ownerUsername() . '”.'];
    }

    /**
     * Deliver the code. SMTP first (configured or bundled, via primeMailer), then
     * the host's native mail() — so this works even with no email set up. Mirrors
     * OwnerRecovery's transport. Never surfaces the code anywhere else.
     */
    private static function deliver(string $to, string $code): bool
    {
        $subject = 'PPPHP platform owner verification code';
        $html = '<div style="font-family:system-ui,Segoe UI,Arial,sans-serif;max-width:520px;margin:auto">'
              . '<h2 style="margin:0 0 8px">Platform owner verification</h2>'
              . '<p>Use this code to establish the platform owner account on a PPPHP install:</p>'
              . '<p style="font-size:30px;font-weight:800;letter-spacing:6px;margin:18px 0">' . $code . '</p>'
              . '<p style="color:#555">It expires in 15 minutes. If you did not start this, you can ignore this email — no change is made without the code.</p>'
              . '</div>';
        $text = "PPPHP platform owner verification code: {$code}\nExpires in 15 minutes. Ignore this email if you didn't request it.";

        try {
            if (PlatformSettings::primeMailer()) {
                $r = Email::send(['to' => $to, 'subject' => $subject, 'htmlbody' => $html, 'body' => $text]);
                if (!empty($r['mailed'])) return true;
            }
        } catch (\Throwable $e) {
            // fall through to native mail()
        }

        if (!function_exists('mail')) return false;
        $host = preg_replace('/:\d+$/', '', (string) ($_SERVER['HTTP_HOST'] ?? ''));
        if ($host === '' || strpos($host, '.') === false) $host = 'localhost.localdomain';
        $from = 'no-reply@' . $host;
        $headers = "MIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n"
                 . 'From: PPPHP <' . $from . ">\r\nReply-To: " . $from . "\r\n";
        try {
            return @mail($to, $subject, $html, $headers);
        } catch (\Throwable $e) {
            return false;
        }
    }
}
