<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Rest;

// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_value
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter
// Custom plugin tables (prli_*): table names interpolated from $wpdb->prefix (trusted),
// user values bind through $wpdb->prepare(). No caching: these tables are the source
// of truth for click/redirect data and must read-through. "meta_key"/"meta_value" here
// refer to our own prli_link_metas table, not wp_postmeta.
use PrettyLinks\Options\Store as LiteOptionsStore;
use PrettyLinks\Pro\Options\Store as OptionsStore;
use PrettyLinks\Pro\PrettyBar\Settings;
use PrettyLinks\Pro\PrettyBar\Templates\BottomBarTemplate;
use PrettyLinks\Pro\PrettyBar\Templates\FloatingTemplate;
use PrettyLinks\Pro\PrettyBar\Templates\TopBarTemplate;
use PrettyLinks\Rest\Controllers\BaseController;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * Pretty Bar settings at `/pretty-links/v1/pro/prettybar/settings`.
 *
 * Response shape:
 *   {
 *     shared: {
 *       active, logo_attachment_id, logo_url,
 *       hide_attribution, attribution_url,
 *       link_count, legacy_link_count, has_legacy_links
 *     },
 *     templates: {
 *       'top-bar':    { ...new v4 schema },
 *       'bottom-bar': { ... },
 *       'floating':   { ... }
 *     },
 *     catalog: [ { id, label } … ]
 *   }
 *
 * Shared settings persist to the exact v3 option rows so a v4→v3
 * downgrade keeps rendering the same bar:
 *   - `prlipro_prettybar_active` (standalone option)
 *   - `prli_options.prettybar_image_url`        — resolved logo URL
 *   - `prlipro_options.prettybar_logo_attachment_id` — new in 4.0
 *   - `prlipro_options.prettybar_hide_attrib_link`
 *   - `prlipro_options.prettybar_attrib_url`
 *
 * Per-template settings go into a nested `prettybar_templates` array on
 * `prlipro_options`. Classic (v3) template config isn't exposed here —
 * it's grandfathered via the v3 option rows directly.
 */
class PrettyBarController extends BaseController
{
    private const COLOR_KEYS = ['bgColor', 'bgColor2', 'textColor', 'accentColor', 'ctaTextColor'];
    private const ENUM_KEYS  = [
        'bgType'   => ['solid', 'gradient'],
        'shadow'   => ['none', 'soft', 'medium', 'strong'],
        'height'   => ['compact', 'standard', 'tall'],
        'position' => [
            'top-left',
            'top-center',
            'top-right',
            'bottom-left',
            'bottom-center',
            'bottom-right',
        ],
    ];
    private const INT_RANGES = [
        'bgAngle'      => [0, 359],
        'bgOpacity'    => [0, 100],
        'borderRadius' => [0, 60],
        'logoHeight'   => [12, 72],
        'titleLimit'   => [5, 500],
        'descLimit'    => [5, 1000],
        'urlLimit'     => [5, 500],
    ];

    /**
     * Register the Pretty Bar settings REST route.
     *
     * @return void
     */
    public function register(): void
    {
        register_rest_route($this->namespace(), '/pro/prettybar/settings', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [$this, 'show'],
                'permission_callback' => $this->permission(),
            ],
            [
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => [$this, 'update'],
                'permission_callback' => $this->permission(),
            ],
        ]);
    }

    /**
     * Return the current Pretty Bar settings.
     *
     * @param WP_REST_Request $request Incoming REST request.
     *
     * @return WP_REST_Response
     */
    public function show(WP_REST_Request $request): WP_REST_Response
    {
        unset($request);
        return new WP_REST_Response($this->currentSettings());
    }

    /**
     * Persist submitted Pretty Bar settings and return the new state.
     *
     * @param WP_REST_Request $request Incoming REST request.
     *
     * @return WP_REST_Response
     */
    public function update(WP_REST_Request $request): WP_REST_Response
    {
        $body  = (array) $request->get_json_params();
        $store = $this->store();

        if (isset($body['shared']) && is_array($body['shared'])) {
            $this->persistShared($store, $body['shared']);
        }

        if (isset($body['classic']) && is_array($body['classic'])) {
            $this->persistClassic($body['classic']);
        }

        if (isset($body['templates']) && is_array($body['templates'])) {
            $existing = (array) ($store->getPro('prettybar_templates', []) ?? []);
            foreach (self::templateDefaults() as $id => $defaults) {
                if (!isset($body['templates'][$id]) || !is_array($body['templates'][$id])) {
                    continue;
                }
                $existing[$id] = $this->sanitizeTemplate($body['templates'][$id], $defaults);
            }
            $store->mergePro(['prettybar_templates' => $existing]);
        }

        return new WP_REST_Response($this->currentSettings());
    }

    /**
     * Default settings keyed by Pretty Bar template id.
     *
     * @return array<string, array<string, mixed>>
     */
    private static function templateDefaults(): array
    {
        return [
            TopBarTemplate::ID    => TopBarTemplate::defaults(),
            BottomBarTemplate::ID => BottomBarTemplate::defaults(),
            FloatingTemplate::ID  => FloatingTemplate::defaults(),
        ];
    }

    /**
     * Assemble the full Pretty Bar settings payload.
     *
     * @return array<string, mixed>
     */
    private function currentSettings(): array
    {
        $shared                      = Settings::shared();
        $shared['link_count']        = $this->prettyBarLinkCount(null);
        $shared['legacy_link_count'] = $this->prettyBarLinkCount('legacy');
        $shared['has_legacy_links']  = Settings::hasLegacyLinks();

        return [
            'shared'    => $shared,
            'templates' => [
                TopBarTemplate::ID    => Settings::forTemplate(TopBarTemplate::ID, TopBarTemplate::defaults()),
                BottomBarTemplate::ID => Settings::forTemplate(BottomBarTemplate::ID, BottomBarTemplate::defaults()),
                FloatingTemplate::ID  => Settings::forTemplate(FloatingTemplate::ID, FloatingTemplate::defaults()),
            ],
            'classic'   => $this->classicSettings(),
            'catalog'   => [
                [
                    'id'    => TopBarTemplate::ID,
                    'label' => (new TopBarTemplate())->label(),
                ],
                [
                    'id'    => BottomBarTemplate::ID,
                    'label' => (new BottomBarTemplate())->label(),
                ],
                [
                    'id'    => FloatingTemplate::ID,
                    'label' => (new FloatingTemplate())->label(),
                ],
            ],
        ];
    }

    /**
     * Snapshot of every v3 Classic Pretty Bar setting, read from the
     * option rows v3 wrote to (lite for visual/content, pro for
     * attribution). Hex colors come back `#`-prefixed so the picker
     * accepts them; v3 stored them bare, so we don't rewrite on read.
     *
     * @return array<string, mixed>
     */
    private function classicSettings(): array
    {
        $lite = new LiteOptionsStore();
        return [
            'title_limit'      => (int) $lite->get('prettybar_title_limit', 25),
            'desc_limit'       => (int) $lite->get('prettybar_desc_limit', 30),
            'link_limit'       => (int) $lite->get('prettybar_link_limit', 30),
            'show_title'       => (bool) $lite->get('prettybar_show_title', '1'),
            'show_description' => (bool) $lite->get('prettybar_show_description', '1'),
            'show_share'       => (bool) $lite->get('prettybar_show_share_links', '1'),
            'show_target_url'  => (bool) $lite->get('prettybar_show_target_url_link', '1'),
            'background_image' => (string) $lite->get('prettybar_background_image_url', ''),
            'bg_color'         => self::hashHex((string) $lite->get('prettybar_color', '')),
            'text_color'       => self::hashHex((string) $lite->get('prettybar_text_color', '000000')),
            'link_color'       => self::hashHex((string) $lite->get('prettybar_link_color', '0000ee')),
            'hover_color'      => self::hashHex((string) $lite->get('prettybar_hover_color', 'ababab')),
            'visited_color'    => self::hashHex((string) $lite->get('prettybar_visited_color', '551a8b')),
        ];
    }

    /**
     * Prefix a hex color string with '#', normalising empty input.
     *
     * @param string $v Raw hex color value.
     *
     * @return string
     */
    private static function hashHex(string $v): string
    {
        $v = trim($v);
        if ($v === '' || $v === '#') {
            return '';
        }
        return $v[0] === '#' ? $v : '#' . $v;
    }

    /**
     * Split writes across v3's option rows so a v4→v3 downgrade finds
     * every shared setting in the same place v3 expects. The new
     * `prettybar_logo_attachment_id` sits alongside the v3 URL key —
     * we write both, always, so either reader gets the right image.
     *
     * @param OptionsStore         $store Pro options store to write to.
     * @param array<string, mixed> $input Submitted shared settings.
     */
    private function persistShared(OptionsStore $store, array $input): void
    {
        $lite = new LiteOptionsStore();

        if (array_key_exists('active', $input)) {
            update_option(Settings::ACTIVE_OPTION, $input['active'] ? 1 : 0);
        }

        // Shared logo. Attachment ID lives on the Pro store; the resolved
        // URL is mirrored to the v3-canonical `prli_options` row so the
        // Classic renderer (and any v4→v3 downgrade) keeps rendering the
        // same image without any migration step.
        if (array_key_exists('logo_attachment_id', $input)) {
            $id  = (int) $input['logo_attachment_id'];
            $url = '';
            if ($id > 0) {
                $resolved = wp_get_attachment_url($id);
                if (is_string($resolved) && $resolved !== '') {
                    $url = $resolved;
                }
            } elseif (array_key_exists('logo_url', $input)) {
                // Attachment cleared; honor an explicit URL if provided.
                $url = esc_url_raw((string) $input['logo_url']);
            }
            $store->mergePro(['prettybar_logo_attachment_id' => $id]);
            $lite->merge(['prettybar_image_url' => $url]);
        } elseif (array_key_exists('logo_url', $input)) {
            // URL-only update (e.g. Classic-settings section edit).
            $lite->merge(['prettybar_image_url' => esc_url_raw((string) $input['logo_url'])]);
        }

        // Attribution settings — Pro store per v3.
        $proPatch = [];
        if (array_key_exists('hide_attribution', $input)) {
            $proPatch['prettybar_hide_attrib_link'] = (bool) $input['hide_attribution'];
        }
        if (array_key_exists('attribution_url', $input)) {
            $proPatch['prettybar_attrib_url'] = esc_url_raw((string) $input['attribution_url']);
        }
        if ($proPatch) {
            $store->mergePro($proPatch);
        }
    }

    /**
     * Persist Classic (v3) Pretty Bar settings to the exact option rows
     * v3 writes to (`prli_options`). Every key is v3-canonical so a
     * v4→v3 downgrade keeps rendering with the same configuration.
     * Logo / attribution / active live on `shared` — not here.
     *
     * @param array<string, mixed> $input Submitted Classic settings.
     */
    private function persistClassic(array $input): void
    {
        $lite  = new LiteOptionsStore();
        $patch = [];

        foreach (
            [
                'title_limit' => 'prettybar_title_limit',
                'desc_limit'  => 'prettybar_desc_limit',
                'link_limit'  => 'prettybar_link_limit',
            ] as $in => $opt
        ) {
            if (array_key_exists($in, $input)) {
                $patch[$opt] = (string) max(0, (int) $input[$in]);
            }
        }

        foreach (
            [
                'show_title'       => 'prettybar_show_title',
                'show_description' => 'prettybar_show_description',
                'show_share'       => 'prettybar_show_share_links',
                'show_target_url'  => 'prettybar_show_target_url_link',
            ] as $in => $opt
        ) {
            if (array_key_exists($in, $input)) {
                // V3 stored these as '1' / '' — mirror that shape exactly.
                $patch[$opt] = $input[$in] ? '1' : '';
            }
        }

        if (array_key_exists('background_image', $input)) {
            $patch['prettybar_background_image_url'] = esc_url_raw((string) $input['background_image']);
        }

        // V3 stored color values bare (e.g. "000000"). We strip the `#` the
        // client sends so a downgraded v3 install still parses them.
        foreach (
            [
                'bg_color'      => 'prettybar_color',
                'text_color'    => 'prettybar_text_color',
                'link_color'    => 'prettybar_link_color',
                'hover_color'   => 'prettybar_hover_color',
                'visited_color' => 'prettybar_visited_color',
            ] as $in => $opt
        ) {
            if (array_key_exists($in, $input)) {
                $v           = trim((string) $input[$in]);
                $patch[$opt] = preg_match('/^#?([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/', $v) === 1
                    ? ltrim($v, '#')
                    : '';
            }
        }

        if ($patch) {
            $lite->merge($patch);
        }
    }

    /**
     * Coerce each field the client sent into the correct type, bounded
     * by the schema. Unknown keys are dropped — clients can't inject
     * arbitrary keys into the stored options blob.
     *
     * @param  array<string, mixed> $input    Submitted template settings.
     * @param  array<string, mixed> $defaults Default template settings to bound against.
     * @return array<string, mixed>
     */
    private function sanitizeTemplate(array $input, array $defaults): array
    {
        $out = [];
        foreach ($defaults as $key => $default) {
            if (!array_key_exists($key, $input)) {
                continue;
            }
            $value = $input[$key];

            if (isset(self::ENUM_KEYS[$key])) {
                $v         = (string) $value;
                $out[$key] = in_array($v, self::ENUM_KEYS[$key], true) ? $v : (string) $default;
                continue;
            }
            if (isset(self::INT_RANGES[$key])) {
                [$min, $max] = self::INT_RANGES[$key];
                $out[$key]   = max($min, min($max, (int) $value));
                continue;
            }
            if (in_array($key, self::COLOR_KEYS, true)) {
                $trimmed = trim((string) $value);
                if (preg_match('/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/', $trimmed) === 1) {
                    $out[$key] = $trimmed;
                }
                continue;
            }
            if (is_bool($default)) {
                $out[$key] = (bool) $value;
                continue;
            }
            if (is_int($default)) {
                $out[$key] = (int) $value;
                continue;
            }
            $out[$key] = sanitize_text_field((string) $value);
        }
        return $out;
    }

    /**
     * Count links currently using Pretty Bar.
     *   - null    → every prettybar link (legacy + modern).
     *   - 'legacy' → links using the v3 Classic renderer (no prettybar_template meta).
     *
     * @param string|null $kind Which subset to count: null for all, 'legacy' for Classic-only.
     *
     * @return integer
     */
    private function prettyBarLinkCount(?string $kind): int
    {
        global $wpdb;
        $links = $wpdb->prefix . 'prli_links';
        if ($kind === 'legacy') {
            $metas = $wpdb->prefix . 'prli_link_metas';
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $count = $wpdb->get_var(
                "SELECT COUNT(*) FROM {$links} l
                 LEFT JOIN {$metas} m
                   ON m.link_id = l.id AND m.meta_key = 'prettybar_template'
                 WHERE l.redirect_type = 'prettybar'
                   AND l.deleted_at IS NULL
                   AND (m.meta_value IS NULL OR m.meta_value = '')"
            );
            return (int) $count;
        }
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $count = $wpdb->get_var(
            "SELECT COUNT(*) FROM {$links} WHERE redirect_type = 'prettybar' AND deleted_at IS NULL"
        );
        return (int) $count;
    }

    /**
     * Resolve the Pro options store from the container.
     *
     * @return OptionsStore
     */
    private function store(): OptionsStore
    {
        return $this->container->has(OptionsStore::class)
            ? $this->container->get(OptionsStore::class)
            : new OptionsStore();
    }
}
