<?php

declare(strict_types=1);

namespace Instinctor\Core;

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

/**
 * Config — Instinctor option storage abstraction.
 *
 * Wraps all instinctor_* option reads and writes.
 * WordPress core options (admin_email, date_format, etc.) are NOT
 * routed through this class — they stay as direct get_option() calls.
 *
 * On WordPress (default): delegates to get_option / update_option / delete_option.
 * On standalone:          reads/writes own instinctor_config table via DB class.
 *
 * Usage:
 *   Config::get('instinctor_currency', 'USD')   → get_option('instinctor_currency', 'USD')
 *   Config::set('instinctor_currency', 'EUR')   → update_option('instinctor_currency', 'EUR')
 *   Config::delete('instinctor_currency')       → delete_option('instinctor_currency')
 *
 * Migration path: replace get_option('instinctor_*') with Config::get('instinctor_*')
 * one file at a time. Behaviour is identical on WordPress.
 */
final class Config
{
    private static ?Config\DriverInterface $driver = null;

    /** @var array<int,array<string,mixed>> Per-owner scoped config, warmed lazily per request. */
    private static array $ownerCache = [];

    /** Sentinel distinguishing "owner stored no override" from a stored false/null. */
    private const MISS = "\0__instinctor_config_miss__\0";

    /**
     * Keys that are per-business (a tenant may own their own value). When a
     * tenant context is active (see Tenant::currentOwnerId) these resolve from
     * instinctor_owner_config first, falling back to the global value. Every
     * other key stays global (billing, licensing, storage, addon, infra).
     * Keyed for O(1) isset() lookup.
     */
    private const TENANT_KEYS = [
        // Store identity + contact
        'instinctor_store_name'          => true,
        'instinctor_store_description'   => true,
        'instinctor_store_type'          => true,
        'instinctor_store_email'         => true,
        'instinctor_store_phone'         => true,
        'instinctor_store_city'          => true,
        'instinctor_store_state'         => true,
        'instinctor_store_postcode'      => true,
        'instinctor_country'             => true,
        'instinctor_store_from_name'     => true,
        'instinctor_store_from_email'    => true,
        // Currency + number format
        'instinctor_currency'            => true,
        'instinctor_currency_symbol'     => true,
        'instinctor_currency_position'   => true,
        'instinctor_thousand_separator'  => true,
        // Branding
        'instinctor_favicon_url'         => true,
        'instinctor_site_logo_url'       => true,
        'instinctor_front_page_id'       => true,
        'instinctor_header_templates'    => true,
        'instinctor_footer_templates'    => true,
        'instinctor_card_templates'      => true,
        // SEO + social
        'instinctor_seo_localbusiness_data'    => true,
        'instinctor_seo_localbusiness_enabled' => true,
        'instinctor_seo_og_image_url'    => true,
        'instinctor_seo_social_facebook' => true,
        'instinctor_seo_social_instagram'=> true,
        'instinctor_seo_social_twitter'  => true,
        'instinctor_seo_social_linkedin' => true,
        'instinctor_seo_social_youtube'  => true,
        // Chat (the per-page/per-owner agent)
        // Where an owner is told a lead came in. Tenant-scoped or every tenant's form
        // submissions and quote requests text the VENDOR instead of the owner who
        // earned them. No-op on standalone, where the owner is NULL either way.
        'instinctor_form_notify_sms'          => true,
        // The owner's own Google review link + the text that carries it
        // (/admin/review-request, which is token-authed so it works from a phone).
        // Unscoped, these sent a tenant's customer to review INSTINCTOR on Google.
        //
        // NOTE the fallback: Config::get() resolves a TENANT_KEY to the vendor's value
        // when the tenant has not set one, which is right for a currency and WRONG here.
        // Any consumer must read these with Config::ownerOnly() in tenant context, or an
        // owner who never pasted their link silently promotes ours. Same trap as
        // instinctor_form_notify_sms (see EstimateService::notifyOwnerOfRequest).
        'instinctor_google_review_url'         => true,
        'instinctor_review_request_message'    => true,
        'instinctor_chat_sms_number'          => true,
        'instinctor_chat_email_address'       => true,
        'instinctor_chat_notify_every_message'=> true,
        'instinctor_chat_quick_actions'       => true,
        'instinctor_chat_ai_enabled'          => true,   // per-owner "answer visitors automatically" toggle
        'instinctor_chat_ai_store_info'       => true,   // per-owner assistant instructions
        'instinctor_chat_ai_tone'             => true,
        // Booking (per-owner hours/capacity + notify)
        'instinctor_booking_settings'         => true,
        // Legacy store-address mirror (written by BusinessProfile; must read per-owner like city/postcode)
        'instinctor_store_address'            => true,
        // Commerce behaviour
        'instinctor_tax_enabled'         => true,
        'instinctor_low_stock_threshold' => true,
        'instinctor_loyalty_enabled'     => true,
        'instinctor_loyalty_points_value'=> true,
        'instinctor_payment_banner'      => true,
        'instinctor_kitchen_token'       => true,
    ];

    // -------------------------------------------------------------------------
    // Driver bootstrap
    // -------------------------------------------------------------------------

    private static function driver(): Config\DriverInterface
    {
        if (self::$driver === null) {
            $env = defined('INSTINCTOR_ENV') ? INSTINCTOR_ENV : 'wordpress';

            self::$driver = $env === 'standalone'
                ? new Config\StandaloneDriver()
                : new Config\WordPressDriver();
        }

        return self::$driver;
    }

    /**
     * Reset the driver — for tests or forced re-initialisation.
     * Not called in production code.
     */
    public static function reset(): void
    {
        self::$driver = null;
    }

    // -------------------------------------------------------------------------
    // Public API
    // -------------------------------------------------------------------------

    /**
     * Read an instinctor_* option value.
     *
     * @param  string $key      Full option name, e.g. 'instinctor_currency'
     * @param  mixed  $default  Returned when the option does not exist
     * @return mixed
     */
    public static function get(string $key, mixed $default = false): mixed
    {
        // Tenant-aware: whitelisted per-business keys resolve to the current
        // tenant's override when one exists, else fall through to the global
        // value. Non-whitelisted keys and the vendor context are untouched.
        if (isset(self::TENANT_KEYS[$key])) {
            $ownerId = Tenant::currentOwnerId();
            if ($ownerId !== null) {
                $scoped = self::ownerGet($ownerId, $key);
                if ($scoped !== self::MISS) {
                    return $scoped;
                }
            }
        }
        return self::driver()->get($key, $default);
    }

    /**
     * Write an instinctor_* option value.
     * Creates the option if it does not exist (mirrors update_option behaviour).
     *
     * @param  string $key    Full option name
     * @param  mixed  $value  Value to store (scalar, array, or object — will be serialised)
     * @return bool           True on success or if value is unchanged
     */
    public static function set(string $key, mixed $value): bool
    {
        return self::driver()->set($key, $value);
    }

    /**
     * Delete an instinctor_* option entirely.
     *
     * @param  string $key  Full option name
     * @return bool         True on success
     */
    public static function delete(string $key): bool
    {
        return self::driver()->delete($key);
    }

    // -------------------------------------------------------------------------
    // Per-owner (tenant) settings
    // -------------------------------------------------------------------------

    /**
     * Read a setting for a specific tenant: the owner's override if present,
     * otherwise the global value (or $default). ownerId <= 0 means the vendor
     * context — resolves globally.
     */
    public static function getForOwner(int $ownerId, string $key, mixed $default = false): mixed
    {
        if ($ownerId > 0) {
            $scoped = self::ownerGet($ownerId, $key);
            if ($scoped !== self::MISS) {
                return $scoped;
            }
        }
        return self::driver()->get($key, $default);
    }

    /**
     * The owner's OWN value, with no global fallback.
     *
     * getForOwner() resolves to the vendor's value when a tenant has not set a key.
     * That is correct for a default (currency, date format): a tenant who set nothing
     * should get USD. It is a leak for anything that ADDRESSES A PERSON: a tenant with
     * no notify number configured would silently inherit the vendor's, and every lead
     * they earned would text the vendor instead of them.
     *
     * Use this for phone numbers, email recipients, and anything else where "not set"
     * must mean "nobody" rather than "fall back to us".
     */
    public static function ownerOnly(int $ownerId, string $key, mixed $default = false): mixed
    {
        if ($ownerId <= 0) {
            return $default;
        }
        $scoped = self::ownerGet($ownerId, $key);
        return $scoped === self::MISS ? $default : $scoped;
    }

    /**
     * Write a tenant-scoped override. ownerId <= 0 falls back to a global set()
     * so callers can pass the resolved owner without special-casing the vendor.
     */
    public static function setForOwner(int $ownerId, string $key, mixed $value): bool
    {
        if ($ownerId <= 0) {
            return self::set($key, $value);
        }
        $result = DB::replace(self::ownerTable(), [
            'owner_id'   => $ownerId,
            'config_key' => $key,
            'config_val' => self::encode($value),
        ]);
        if ($result !== false) {
            self::$ownerCache[$ownerId][$key] = $value;
            return true;
        }
        return false;
    }

    /** Remove a tenant override so the key reverts to the global value. */
    public static function deleteForOwner(int $ownerId, string $key): bool
    {
        if ($ownerId <= 0) {
            return self::delete($key);
        }
        $result = DB::delete(self::ownerTable(), ['owner_id' => $ownerId, 'config_key' => $key]);
        unset(self::$ownerCache[$ownerId][$key]);
        return $result !== false;
    }

    // ── Internal: owner-scoped store ────────────────────────────────────────

    private static function ownerTable(): string
    {
        return DB::prefix() . 'instinctor_owner_config';
    }

    /** Owner override for a key, or self::MISS when the owner has none. */
    private static function ownerGet(int $ownerId, string $key): mixed
    {
        self::warmOwner($ownerId);
        $bucket = self::$ownerCache[$ownerId] ?? [];
        return array_key_exists($key, $bucket) ? $bucket[$key] : self::MISS;
    }

    /** Load all of an owner's overrides in one query, once per request. */
    private static function warmOwner(int $ownerId): void
    {
        if (isset(self::$ownerCache[$ownerId])) {
            return;
        }
        self::$ownerCache[$ownerId] = [];
        try {
            $rows = DB::get_results(DB::prepare(
                "SELECT config_key, config_val FROM `" . self::ownerTable() . "` WHERE owner_id = %d",
                $ownerId
            ));
            foreach ($rows as $row) {
                self::$ownerCache[$ownerId][$row->config_key] = self::decode($row->config_val);
            }
        } catch (\Throwable) {
            // Table absent (pre-migration) — treat as no overrides.
        }
    }

    private static function encode(mixed $value): string
    {
        return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    private static function decode(string $encoded): mixed
    {
        if ($encoded === '') {
            return false;
        }
        $decoded = json_decode($encoded, true);
        return ($decoded !== null || $encoded === 'null') ? $decoded : $encoded;
    }

    /**
     * Return the base URL for a REST route, environment-aware.
     *
     * On WordPress:   delegates to rest_url() → /wp-json/{route}
     * On standalone:  /api/{route}
     *
     * @param  string $route  Route path, e.g. 'instinctor/v1/' or 'ilm/v1/shapes'
     * @return string
     */
    public static function restUrl(string $route = ''): string
    {
        if (!defined('INSTINCTOR_ENV') || INSTINCTOR_ENV === 'wordpress') {
            return rest_url($route);
        }
        // Standalone: API lives at /api/{route}
        $base = rtrim(home_url('/api'), '/') . '/';
        return $base . ltrim($route, '/');
    }
}
