<?php

declare(strict_types=1);

namespace Instinctor\Services;

use Instinctor\Core\Config;

if (!defined('ABSPATH')) exit;

/**
 * OwnerAccess — the site owner's way into the mobile control panel (InCommand).
 *
 * On instinctor.com a tenant signs in with the email they published with. A
 * designer-built site has no such account: the WordPress install belongs to the
 * designer, and the owner is somebody who has never seen wp-admin and never
 * should. This is the bridge. The designer turns the panel on in Settings, names
 * the owner, and sends one link. The owner taps it on their phone and is in.
 *
 * The link carries a signed token, not a password:
 *
 *   base64url(payload) . "." . hmac_sha256(base64url(payload), secret)
 *
 * The payload binds the token to one user, one host and one expiry, so a link
 * copied off a phone cannot be replayed against another install or used forever.
 * Revocation rotates the secret, which invalidates every outstanding link at
 * once: there is no per-token store to keep in sync, and "revoke" is the honest
 * answer to "my phone was stolen".
 *
 * @since 2.1.3123
 */
final class OwnerAccess
{
    private const K_ENABLED = 'instinctor_panel_enabled';
    private const K_OWNER   = 'instinctor_panel_owner_user';
    private const K_SECRET  = 'instinctor_panel_link_secret';
    private const K_SENT    = 'instinctor_panel_link_sent_at';

    /** How long a freshly minted link stays usable. */
    private const LINK_TTL = 14 * DAY_IN_SECONDS;

    /** How long the owner stays signed in after tapping it. Phones, not desks. */
    public const SESSION_DAYS = 30;

    // ── State ────────────────────────────────────────────────────────────────

    public static function enabled(): bool
    {
        return (string) Config::get(self::K_ENABLED, '0') === '1';
    }

    public static function setEnabled(bool $on): void
    {
        Config::set(self::K_ENABLED, $on ? '1' : '0');
    }

    /** The user this site's panel belongs to, or 0 when nobody is named yet. */
    public static function ownerUserId(): int
    {
        return max(0, (int) Config::get(self::K_OWNER, '0'));
    }

    public static function ownerEmail(): string
    {
        $id = self::ownerUserId();
        if ($id <= 0) return '';
        $u = self::userById($id);
        return (string) ($u->user_email ?? $u->email ?? '');
    }

    /** When the last link was minted, for "sent 3 days ago" in the UI. */
    public static function lastSentAt(): string
    {
        return (string) Config::get(self::K_SENT, '');
    }

    // ── The owner's account ──────────────────────────────────────────────────

    /**
     * Find or create the account the panel signs in. Returns [userId, error].
     *
     * Deliberately the lowest-privilege account WordPress offers: the panel does
     * its own gating, so the owner needs no WP capability at all. If the email
     * already belongs to somebody (often the designer themselves), that account
     * is reused rather than duplicated.
     */
    public static function assignOwner(string $email, string $name = ''): array
    {
        $email = sanitize_email(trim($email));
        if ($email === '' || !is_email($email)) {
            return [0, 'That email address does not look right.'];
        }

        $existing = self::userByEmail($email);
        if ($existing) {
            $id = (int) ($existing->ID ?? $existing->id ?? 0);
            if ($id > 0) {
                Config::set(self::K_OWNER, (string) $id);
                return [$id, ''];
            }
        }

        $id = self::createUser($email, $name);
        if ($id <= 0) return [0, 'Could not create the owner account.'];
        Config::set(self::K_OWNER, (string) $id);
        return [$id, ''];
    }

    // ── The link ─────────────────────────────────────────────────────────────

    /** A fresh sign-in link for the named owner, or '' when nobody is named. */
    public static function mintLink(): string
    {
        $uid = self::ownerUserId();
        if ($uid <= 0) return '';

        $payload = self::b64(wp_json_encode([
            'u' => $uid,
            'h' => self::host(),
            'e' => time() + self::LINK_TTL,
            'j' => bin2hex(random_bytes(6)),
        ]));
        $token = $payload . '.' . hash_hmac('sha256', $payload, self::secret());
        Config::set(self::K_SENT, gmdate('c'));

        return self::siteUrl() . '/dashboard/panel-login?t=' . rawurlencode($token);
    }

    /**
     * Validate a token and return the user id it names, or 0.
     *
     * Checks the signature before anything else and compares it in constant
     * time, so a forged token cannot be tuned by watching how fast we say no.
     */
    public static function verifyToken(string $token): int
    {
        if (!self::enabled()) return 0;
        $parts = explode('.', $token, 2);
        if (count($parts) !== 2) return 0;
        [$payload, $sig] = $parts;

        $expected = hash_hmac('sha256', $payload, self::secret());
        if (!hash_equals($expected, $sig)) return 0;

        $data = json_decode((string) self::unb64($payload), true);
        if (!is_array($data)) return 0;
        if ((int) ($data['e'] ?? 0) < time()) return 0;
        if ((string) ($data['h'] ?? '') !== self::host()) return 0;

        $uid = (int) ($data['u'] ?? 0);
        if ($uid <= 0 || $uid !== self::ownerUserId()) return 0;   // owner reassigned = old links die

        return $uid;
    }

    /** Invalidate every outstanding link. */
    public static function revokeLinks(): void
    {
        Config::set(self::K_SECRET, self::newSecret());
    }

    // ── Internals ────────────────────────────────────────────────────────────

    private static function secret(): string
    {
        $s = (string) Config::get(self::K_SECRET, '');
        if ($s === '') {
            $s = self::newSecret();
            Config::set(self::K_SECRET, $s);
        }
        return $s;
    }

    private static function newSecret(): string
    {
        return bin2hex(random_bytes(32));
    }

    /** Host the token is bound to, so a link cannot be replayed elsewhere. */
    private static function host(): string
    {
        $u = self::siteUrl();
        return (string) (wp_parse_url($u, PHP_URL_HOST) ?: $u);
    }

    private static function siteUrl(): string
    {
        if (function_exists('home_url')) return rtrim((string) home_url(), '/');
        return rtrim((string) (defined('APP_URL') ? APP_URL : ''), '/');
    }

    private static function b64(string $s): string
    {
        return rtrim(strtr(base64_encode($s), '+/', '-_'), '=');
    }

    private static function unb64(string $s): string
    {
        return (string) base64_decode(strtr($s, '-_', '+/'), true);
    }

    // ── The two environments ─────────────────────────────────────────────────

    private static function standalone(): bool
    {
        return defined('INSTINCTOR_ENV') && INSTINCTOR_ENV === 'standalone';
    }

    private static function userByEmail(string $email): ?object
    {
        if (!self::standalone() && function_exists('get_user_by')) {
            $u = get_user_by('email', $email);
            return $u ?: null;
        }
        $t = \Instinctor\Core\DB::prefix() . 'instinctor_users';
        $u = \Instinctor\Core\DB::get_row(\Instinctor\Core\DB::prepare(
            "SELECT * FROM {$t} WHERE email = %s LIMIT 1", $email));
        return $u ?: null;
    }

    private static function userById(int $id): ?object
    {
        if (!self::standalone() && function_exists('get_user_by')) {
            $u = get_user_by('id', $id);
            return $u ?: null;
        }
        $t = \Instinctor\Core\DB::prefix() . 'instinctor_users';
        $u = \Instinctor\Core\DB::get_row(\Instinctor\Core\DB::prepare(
            "SELECT * FROM {$t} WHERE id = %d LIMIT 1", $id));
        return $u ?: null;
    }

    private static function createUser(string $email, string $name): int
    {
        $display = $name !== '' ? $name : (string) strstr($email, '@', true);

        if (!self::standalone() && function_exists('wp_insert_user')) {
            $login = self::freeLogin($email);
            $id = wp_insert_user([
                'user_login'   => $login,
                'user_email'   => $email,
                'user_pass'    => wp_generate_password(24, true, true),
                'display_name' => $display,
                'first_name'   => $display,
                // 'subscriber' is the floor: the panel gates itself and the owner
                // never sees wp-admin, so no WordPress capability is warranted.
                'role'         => 'subscriber',
            ]);
            return is_wp_error($id) ? 0 : (int) $id;
        }

        $t = \Instinctor\Core\DB::prefix() . 'instinctor_users';
        \Instinctor\Core\DB::query(\Instinctor\Core\DB::prepare(
            "INSERT INTO {$t} (email, password_hash, display_name, role, status, email_verified, meta)
             VALUES (%s, %s, %s, 'site_owner', 'active', 1, '{}')",
            $email, '!' . bin2hex(random_bytes(16)), $display));
        return (int) \Instinctor\Core\DB::get_var(\Instinctor\Core\DB::prepare(
            "SELECT id FROM {$t} WHERE email = %s LIMIT 1", $email));
    }

    private static function freeLogin(string $email): string
    {
        $base = sanitize_user((string) strstr($email, '@', true), true) ?: 'owner';
        $try  = $base;
        $n    = 1;
        while (username_exists($try)) { $try = $base . '-' . (++$n); }
        return $try;
    }

    /**
     * Sign the owner in and return where to send them.
     *
     * WordPress gets a 30-day cookie: an owner who has to sign in again every
     * fortnight on a phone stops opening the app at all, which is the failure
     * mode that cost us weeks with the first tenant who tried it.
     */
    public static function signIn(int $userId): void
    {
        if (self::standalone()) {
            \Instinctor\Core\Auth::loginAs($userId, true);
            return;
        }
        $days = self::SESSION_DAYS;
        add_filter('auth_cookie_expiration', static fn() => $days * DAY_IN_SECONDS, 99);
        wp_set_current_user($userId);
        wp_set_auth_cookie($userId, true);
    }
}
