<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Options;

/**
 * Reads and writes the Pro option blob `prlipro_options`.
 *
 * Every field name is preserved from v3.x — see the back-compat checklist
 * in REWRITE-PLAN.md Appendix A and 05-lite-options.md for provenance.
 *
 * Pro code SHOULD type-hint this class directly rather than the Lite
 * `PrettyLinks\Options\Store` — Lite does not (and must not) know about
 * Pro options.
 */
class Store
{
    public const OPTION = 'prlipro_options';

    /**
     * Back-compat alias — use `::OPTION` in new code.
     */
    public const PRO_OPTION = self::OPTION;

    /**
     * Cached merged option blob, or null until first read.
     *
     * @var array<string, mixed>|null
     */
    private ?array $cache = null;

    /**
     * Pro option defaults.
     *
     * @return array<string, mixed>
     */
    public static function defaults(): array
    {
        return [
            // Default is empty so free installs + pro upgrades from v3
            // (which had empty prefix) keep links at site root. Pro's
            // Bootstrap seeder overrides this to 'go' on a truly fresh
            // pro install (no existing links) as the Pretty Links house
            // default.
            'base_slug_prefix'                         => '',
            'keyword_replacement_is_on'                => false,
            'keyword_case_sensitive'                   => false,
            'keyword_ignore'                           => '',
            // Per-post throttling. Three independent caps; whichever fires
            // first stops further linking on that post. All caps are
            // unlimited when `set_keyword_thresholds` is false.
            // keywords_per_page            — unique keywords per post
            // (v3 drop-in).
            // keyword_links_per_page       — replacements per keyword
            // (v3 drop-in — restored from
            // v3 per-keyword semantics).
            // keyword_total_links_per_page — total links per post,
            // across all keywords (v4
            // addition; 0 = unlimited so
            // v3 sites upgrade with no
            // behavior change).
            'set_keyword_thresholds'                   => false,
            'keywords_per_page'                        => 5,
            'keyword_links_per_page'                   => 2,
            'keyword_total_links_per_page'             => 0,
            // Site-wide forcing of anchor attributes on every keyword link.
            // Override per-link flags when on.
            'keyword_links_nofollow'                   => false,
            'keyword_links_sponsored'                  => false,
            'keyword_links_open_new_window'            => false,
            // Appended after each keyword anchor's closing tag (e.g. "(aff)").
            // `enable_keyword_link_disclosures` is the v3 master gate —
            // disclosure text isn't rendered unless this is on.
            'enable_keyword_link_disclosures'          => false,
            'keyword_link_disclosure'                  => '',
            // V3 pro CSS injected into wp_head for every keyword-replaced
            // anchor on the frontend.
            'keyword_link_custom_css'                  => '',
            'keyword_link_hover_custom_css'            => '',
            // Extend replacement beyond `the_content`. v3's 4-state scope
            // selector, shared between keyword and URL engines:
            // 'none'     — no replacement in this scope
            // 'keywords' — keyword replacement only
            // 'urls'     — URL replacement only
            // 'both'     — both.
            'replace_keywords_in_comments'             => 'none',
            'replace_keywords_in_feeds'                => 'none',
            // Global disclosure appended after replacements (v3 parity).
            'enable_link_to_disclosures'               => false,
            'disclosures_link_url'                     => '',
            'disclosures_link_text'                    => 'Affiliate Link Disclosures',
            'disclosures_link_position'                => 'bottom',
            // V3 two-toggle model:
            // url_replacement_is_on         — master gate; enables
            // per-link explicit URL aliases ("URL Replacements" tab
            // on each link edit page). Off = neither mode runs.
            // replace_urls_with_pretty_links — sub-toggle; when on AND
            // master is on, also rewrites every `<a href>` whose
            // target matches a known pretty-link, honoring the block
            // list below.
            'url_replacement_link_custom_css'          => '',
            'url_replacement_link_hover_custom_css'    => '',
            'url_replacement_is_on'                    => false,
            'replace_urls_with_pretty_links'           => false,
            'url_replace_nofollow'                     => false,
            'url_replace_sponsored'                    => false,
            'url_replace_new_window'                   => false,
            // Newline- or comma-separated URLs excluded from blanket URL
            // replacement. Empty defaults to the current site's hostname
            // variants at runtime (see UrlReplacer::blockList()) so
            // internal links aren't auto-rewritten on fresh installs.
            // Stored key keeps the v3-canonical `_blacklist` suffix for
            // back-compat; internal terminology is block list.
            'replace_urls_with_pretty_links_blacklist' => '',
            // V3 post-type allow-list for URL replacement.
            'url_replacement_cpts'                     => ['post'],
            // Global master. Per-CPT rows live under `autocreate` below and
            // only fire when this is on.
            'autocreate_enable'                        => false,
            // Per-CPT row map: slug => ['enabled' => 0|1, 'category' => int,
            // 'socbtns' => 0|1]. One row per post type the admin has configured
            // anything for. Drives AutoCreator (enabled + category) and
            // SocialButtons\Service (socbtns). Missing slug = all-off for
            // that CPT.
            'autocreate'                               => [],
            // V3 post-type allow-list for keyword replacement. Empty falls
            // back to process_posts/process_pages flags above.
            'keyword_replacement_cpts'                 => ['post'],
            // V3 alternate shortlink domain (pro feature). When
            // `use_prettylink_url` is on, pretty URLs render with the
            // value of `prettylink_url` instead of home_url().
            'use_prettylink_url'                       => false,
            'prettylink_url'                           => '',
            // V3 global toggle for QR code generation on every new link.
            'generate_qr_codes'                        => false,
            // V3 custom JS/CSS injected into cloaked <head>.
            'global_head_scripts'                      => '',
            // Pretty Bar per-template settings live here as a nested map
            // keyed by template id ('top-bar', 'bottom-bar', 'floating').
            // Each template's saved values are overlaid on its own
            // `defaults()` by `Settings::forTemplate`. Empty on fresh
            // installs — the live editor writes the first entry.
            'prettybar_templates'                      => [],
            // WP Media attachment ID for the shared Pretty Bar logo (4.0).
            // The v3-canonical URL companion (`prli_options.prettybar_image_url`)
            // is kept in sync on save so the Classic template + any v4→v3
            // downgrade keep rendering the same image.
            'prettybar_logo_attachment_id'             => 0,
            // V3 shared-row keys we continue to respect (read + write) so
            // Classic renders unchanged and downgrade is lossless.
            'prettybar_title_limit'                    => '25',
            'prettybar_desc_limit'                     => '30',
            'prettybar_link_limit'                     => '30',
            'prettybar_hide_attrib_link'               => false,
            'prettybar_attrib_url'                     => '',
            'qr_logo_attachment_id'                    => 0,
            'qr_logo_size_percent'                     => 20,
            'qr_logo_white_background'                 => true,
            'enable_link_health'                       => false,
            'link_health_frequency'                    => 'weekly',
            'enable_link_health_emails'                => false,
            'link_health_emails'                       => '',
            'link_health_emails_frequency'             => 'weekly',
            'public_link_prefix'                       => 'p',
            // Public (anonymous visitor) link creation via the
            // [prli_create_form] shortcode. When 0, the shortcode +
            // ajax handler no-op.
            // v3 Pro parity — controls which WP capability can access the
            // Pretty Links admin. Stored here (prlipro_options) to match v3's
            // key location so upgrades and downgrades are lossless.
            'min_role'                                 => 'manage_options',
            'allow_public_link_creation'               => false,
            // Optional page ID. When > 0, successful submissions redirect
            // there with ?slug=… so the display shortcodes can render
            // the new link. When 0, a minimal takeover template renders
            // inline (v3 fallback behavior).
            'public_link_display_page'                 => 0,
            'social_buttons_enabled'                   => false,
            'social_buttons_placement'                 => 'bottom',
            'social_buttons_show_in_feed'              => false,
            // Per-button array populated on first save; empty here so
            // Service::resolveButtons() seeds a sensible default set.
            'social_buttons'                           => [],
            'no_enforce_url_scheme'                    => false,
            // Kernel-mode redirect: when true, Pro drops an MU-plugin
            // into wp-content/mu-plugins/ that dispatches pretty-link
            // redirects before the rest of WordPress loads.
            'mu_dispatch'                              => false,
        ];
    }

    /**
     * Return the full merged option blob (defaults overlaid with stored values).
     *
     * @return array<string, mixed>
     */
    public function all(): array
    {
        if ($this->cache === null) {
            $stored = get_option(self::OPTION, []);
            if (!is_array($stored)) {
                $stored = [];
            }
            $this->cache = array_merge(self::defaults(), $stored);

            // V3 stored the base slug prefix as 'prlipro-base-slug-prefix'.
            // On upgrade, carry it forward; fresh installs keep the default.
            if (!array_key_exists('base_slug_prefix', $stored) && !empty($stored)) {
                $this->cache['base_slug_prefix'] = isset($stored['prlipro-base-slug-prefix'])
                    ? (string) $stored['prlipro-base-slug-prefix']
                    : '';
            }
        }
        return $this->cache;
    }

    /**
     * Get a single Pro option value, falling back to a default when unset.
     *
     * @param  string $key     The option key to read.
     * @param  mixed  $default Value returned when the key is absent.
     * @return mixed
     */
    public function get(string $key, $default = null)
    {
        $all = $this->all();
        return array_key_exists($key, $all) ? $all[$key] : $default;
    }

    /**
     * Set a single Pro option value and persist the blob.
     *
     * @param  string $key   The option key to write.
     * @param  mixed  $value The value to store.
     * @return void
     */
    public function set(string $key, $value): void
    {
        $all       = $this->all();
        $all[$key] = $value;
        update_option(self::OPTION, $all);
        $this->cache = $all;
    }

    /**
     * Merge multiple Pro option values into the blob and persist.
     *
     * @param  array<string, mixed> $values The key/value pairs to merge.
     * @return void
     */
    public function merge(array $values): void
    {
        $all = array_merge($this->all(), $values);
        update_option(self::OPTION, $all);
        $this->cache = $all;
    }

    /**
     * Clear the in-memory cache so the next read reloads from the database.
     *
     * @return void
     */
    public function forget(): void
    {
        $this->cache = null;
    }

    // ---- Back-compat aliases. Preserved only while pro callers finish
    // migrating from the old Lite-side `getPro/allPro/setPro/mergePro`
    // shape to the clean Pro Store API. Delete these once no callers remain.

    /**
     * Back-compat alias for `all()`.
     *
     * @return array<string, mixed>
     */
    public function allPro(): array
    {
        return $this->all();
    }

    /**
     * Back-compat alias for `get()`.
     *
     * @param  string $key     The option key to read.
     * @param  mixed  $default Value returned when the key is absent.
     * @return mixed
     */
    public function getPro(string $key, $default = null)
    {
        return $this->get($key, $default);
    }

    /**
     * Back-compat alias for `set()`.
     *
     * @param  string $key   The option key to write.
     * @param  mixed  $value The value to store.
     * @return void
     */
    public function setPro(string $key, $value): void
    {
        $this->set($key, $value);
    }

    /**
     * Back-compat alias for `merge()`.
     *
     * @param  array<string, mixed> $values The key/value pairs to merge.
     * @return void
     */
    public function mergePro(array $values): void
    {
        $this->merge($values);
    }
}
