<?php
/**
 * License client for maca Restu Pro (maca.se shared license server / SFLS API).
 *
 * @package Maca_Menu_List_Pro
 */

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

class Maca_MenuList_License {

    const OPTION_KEY       = 'maca_menulist_pro_license';
    const CRON_HOOK        = 'maca_menulist_pro_license_check';
    const TRANSIENT_PREFIX = 'maca_menulist_license_';

    /**
     * @var self|null
     */
    private static $instance = null;

    /**
     * @return self
     */
    public static function instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        add_action(self::CRON_HOOK, array($this, 'cron_validate'));
        add_action('admin_init', array($this, 'maybe_schedule_cron'));
        add_action('admin_init', array($this, 'maybe_revalidate_trial'), 6);
        add_action('admin_notices', array($this, 'render_comp_expiry_notice'));
        add_action('admin_notices', array($this, 'render_trial_expiry_notice'));
    }

    /**
     * Banner threshold (days before complimentary expiry).
     *
     * @return int
     */
    private function get_comp_expiry_notice_days() {
        $default = defined('MACA_MENULIST_COMP_EXPIRY_NOTICE_DAYS') ? (int) MACA_MENULIST_COMP_EXPIRY_NOTICE_DAYS : 30;
        $days = (int) apply_filters('maca_menulist_comp_expiry_notice_days', $default);

        return max(0, $days);
    }

    /**
     * Whether stored license data represents a complimentary (comp) license.
     *
     * @param array<string, mixed> $data Stored license option.
     * @return bool
     */
    public function is_complimentary_license($data = null) {
        $data = is_array($data) ? $data : $this->get_stored_license();

        if (!empty($data['is_comp'])) {
            return true;
        }

        $plan = isset($data['plan']) ? strtolower(sanitize_text_field((string) $data['plan'])) : '';
        if (in_array($plan, array('complimentary', 'comp'), true)) {
            return true;
        }

        $key = isset($data['license_key']) ? $this->normalize_key((string) $data['license_key']) : '';
        if ($key !== '' && preg_match('/-COMP-/i', $key)) {
            return true;
        }

        return false;
    }

    /**
     * Calendar days until a UTC timestamp (site timezone, start of day).
     *
     * @param int $expires_ts Expiry timestamp.
     * @return int Negative when already expired.
     */
    public function get_calendar_days_until_expiry($expires_ts) {
        $expires_ts = (int) $expires_ts;
        if ($expires_ts <= 0) {
            return 0;
        }

        $timezone = wp_timezone();
        $today = new DateTimeImmutable('today', $timezone);
        $expiry_day = (new DateTimeImmutable('@' . $expires_ts))
            ->setTimezone($timezone)
            ->setTime(0, 0, 0);

        return (int) $today->diff($expiry_day)->format('%r%a');
    }

    /**
     * Resolve expiry timestamp from stored license data.
     *
     * @param array<string, mixed> $data Stored license option.
     * @return int
     */
    public function get_license_expiry_timestamp($data = null) {
        $data = is_array($data) ? $data : $this->get_stored_license();

        if (!empty($data['expires_at_ts'])) {
            return (int) $data['expires_at_ts'];
        }

        if (!empty($data['expires'])) {
            return (int) strtotime((string) $data['expires'] . ' 23:59:59 UTC');
        }

        return 0;
    }

    /**
     * Build admin notice context for an expiring complimentary license.
     *
     * @return array<string, mixed>|null
     */
    public function get_comp_expiry_notice_context() {
        $data = $this->get_stored_license();
        if (empty($data['status']) || $data['status'] !== 'active' || !$this->is_complimentary_license($data)) {
            return null;
        }

        $expires_ts = $this->get_license_expiry_timestamp($data);
        if ($expires_ts <= 0) {
            return null;
        }

        $days_left = $this->get_calendar_days_until_expiry($expires_ts);
        $notice_days = $this->get_comp_expiry_notice_days();

        if ($days_left < 0 || $days_left > $notice_days) {
            return null;
        }

        return array(
            'days_left' => $days_left,
            'expires_ts' => $expires_ts,
            'expires_label' => date_i18n(get_option('date_format'), $expires_ts),
            'plugin_name' => $this->get_plugin_display_name(),
            'subscribe_url' => $this->get_subscribe_url(),
            'subscription_url' => admin_url('admin.php?page=maca-restu-subscription'),
        );
    }

    /**
     * Detect complimentary flag from API payload.
     *
     * @param array<string, mixed> $json API response.
     * @return bool
     */
    private function response_is_complimentary($json) {
        if (!empty($json['is_comp'])) {
            return true;
        }

        $plan = isset($json['plan']) ? strtolower(sanitize_text_field((string) $json['plan'])) : '';

        return in_array($plan, array('complimentary', 'comp'), true);
    }

    /**
     * Normalize expires date and timestamp from API payload.
     *
     * @param array<string, mixed> $json API response.
     * @return array{expires: string, expires_at_ts: int|null}
     */
    private function response_expiry_fields($json) {
        $expires = '';
        $expires_at_ts = null;

        if (!empty($json['expires_at'])) {
            if (is_numeric($json['expires_at'])) {
                $expires_at_ts = (int) $json['expires_at'];
                $expires = gmdate('Y-m-d', $expires_at_ts);
            } else {
                $time = strtotime((string) $json['expires_at']);
                if ($time) {
                    $expires_at_ts = $time;
                    $expires = gmdate('Y-m-d', $time);
                }
            }
        } elseif (!empty($json['expires'])) {
            $expires = sanitize_text_field((string) $json['expires']);
            $expires_at_ts = (int) strtotime($expires . ' 23:59:59 UTC');
        }

        return array(
            'expires' => $expires,
            'expires_at_ts' => $expires_at_ts,
        );
    }

    /**
     * Human-readable plugin name for admin license notices.
     *
     * @return string
     */
    private function get_plugin_display_name() {
        $default = __('maca Restu Pro', 'maca-restu-pro');
        $main_file = defined('MACA_MENULIST_PLUGIN_FILE')
            ? MACA_MENULIST_PLUGIN_FILE
            : (defined('MACA_MENULIST_PLUGIN_DIR')
                ? MACA_MENULIST_PLUGIN_DIR . 'maca-restu-pro.php'
                : '');

        if ($main_file !== '' && is_readable($main_file)) {
            if (!function_exists('get_plugin_data')) {
                require_once ABSPATH . 'wp-admin/includes/plugin.php';
            }

            $plugin_data = get_plugin_data($main_file, false, false);
            if (!empty($plugin_data['Name'])) {
                $default = (string) $plugin_data['Name'];
            }
        }

        return (string) apply_filters('maca_menulist_license_notice_plugin_name', $default);
    }

    /**
     * @return void
     */
    public function init() {
        add_action('wp_ajax_maca_menulist_activate_license', array($this, 'ajax_activate'));
        add_action('wp_ajax_maca_menulist_deactivate_license', array($this, 'ajax_deactivate'));
        add_action('wp_ajax_maca_menulist_start_trial', array($this, 'ajax_start_trial'));
    }

    /**
     * @return void
     */
    public function maybe_schedule_cron() {
        if (!wp_next_scheduled(self::CRON_HOOK)) {
            wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK);
        }
    }

    /**
     * @return void
     */
    public static function clear_cron() {
        $timestamp = wp_next_scheduled(self::CRON_HOOK);
        if ($timestamp) {
            wp_unschedule_event($timestamp, self::CRON_HOOK);
        }
    }

    /**
     * @return bool
     */
    public function is_active() {
        $data = $this->get_stored_license();
        if (empty($data['status']) || $data['status'] !== 'active') {
            return false;
        }

        if ($this->is_trial_license($data)) {
            $checked = !empty($data['checked_at']) ? (int) strtotime($data['checked_at'] . ' UTC') : 0;
            $max_age = (int) apply_filters('maca_menulist_trial_max_check_age', 2 * DAY_IN_SECONDS);
            if ($checked <= 0 || (time() - $checked) > $max_age) {
                return false;
            }

            $expires_ts = $this->get_license_expiry_timestamp($data);
            if ($expires_ts > 0 && time() > $expires_ts) {
                return false;
            }
        }

        return true;
    }

    /**
     * Whether stored license data is a server-controlled free trial.
     *
     * @param array<string, mixed>|null $data Stored license option.
     * @return bool
     */
    public function is_trial_license($data = null) {
        $data = is_array($data) ? $data : $this->get_stored_license();

        if (!empty($data['is_trial'])) {
            return true;
        }

        $plan = isset($data['plan']) ? strtolower(sanitize_text_field((string) $data['plan'])) : '';

        return $plan === 'trial';
    }

    /**
     * Best public site URL for license server calls (Loopia/proxy-safe).
     *
     * @return string
     */
    private function get_public_site_url() {
        if (function_exists('maca_restu_api_resolve_site_url')) {
            return untrailingslashit(maca_restu_api_resolve_site_url());
        }

        return untrailingslashit(home_url());
    }

    /**
     * Stable install fingerprint sent to maca.se (prevents re-trial after reinstall on same URL).
     *
     * @return string
     */
    public function get_site_fingerprint() {
        $auth = defined('AUTH_KEY') ? (string) AUTH_KEY : '';

        return hash(
            'sha256',
            $this->get_public_site_url() . '|' . $auth . '|' . (defined('MACA_MENULIST_VERSION') ? MACA_MENULIST_VERSION : '')
        );
    }

    /**
     * Cached trial config from license server (enabled + days).
     *
     * @param bool $force_refresh Skip transient cache.
     * @return array{enabled:bool,days:int}
     */
    public function get_trial_config($force_refresh = false) {
        $cache_key = self::TRANSIENT_PREFIX . 'trial_config';
        $default   = array(
            'enabled' => false,
            'days'    => 30,
        );

        if (!$force_refresh) {
            $cached = get_transient($cache_key);
            if (is_array($cached)) {
                return array_merge($default, $cached);
            }
        }

        $url = $this->get_trial_api_root() . '/config';
        $response = wp_remote_get(
            $url,
            array(
                'timeout' => 10,
                'headers' => array('Accept' => 'application/json'),
            )
        );

        if (is_wp_error($response)) {
            return $default;
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $json = json_decode(wp_remote_retrieve_body($response), true);
        if ($code < 200 || $code >= 300 || !is_array($json) || empty($json['config']) || !is_array($json['config'])) {
            return $default;
        }

        $config = array(
            'enabled' => !empty($json['config']['enabled']),
            'days'    => max(1, (int) ($json['config']['days'] ?? 30)),
        );

        set_transient($cache_key, $config, 12 * HOUR_IN_SECONDS);

        return $config;
    }

    /**
     * Whether this site may start a one-time Pro trial (server-controlled).
     *
     * @param bool $force_refresh Skip transient cache.
     * @return array{eligible:bool,enabled:bool,days:int,message:string}
     */
    public function get_trial_eligibility($force_refresh = false) {
        $cache_key = self::TRANSIENT_PREFIX . 'trial_eligibility';
        $default   = array(
            'eligible' => false,
            'enabled'  => false,
            'days'     => 30,
            'message'  => '',
        );

        if (!$force_refresh) {
            $cached = get_transient($cache_key);
            if (is_array($cached)) {
                return array_merge($default, $cached);
            }
        }

        $response = wp_remote_post(
            $this->get_trial_api_root() . '/eligibility',
            array(
                'timeout' => 10,
                'headers' => array(
                    'Content-Type' => 'application/json; charset=utf-8',
                    'Accept'       => 'application/json',
                ),
                'body'    => wp_json_encode(
                    array(
                        'site_url'         => $this->get_public_site_url(),
                        'product'          => 'maca_restu_pro',
                        'site_fingerprint' => $this->get_site_fingerprint(),
                    )
                ),
            )
        );

        if (is_wp_error($response)) {
            return $default;
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $json = json_decode(wp_remote_retrieve_body($response), true);

        if ($code < 200 || $code >= 300 || !is_array($json)) {
            return $default;
        }

        $status = array(
            'eligible' => !empty($json['eligible']),
            'enabled'  => !empty($json['enabled']),
            'days'     => max(1, (int) ($json['days'] ?? 30)),
            'message'  => isset($json['message']) ? sanitize_text_field((string) $json['message']) : '',
        );

        set_transient($cache_key, $status, 12 * HOUR_IN_SECONDS);

        return $status;
    }

    /**
     * @return string Trial API root ending with /license/trial.
     */
    private function get_trial_api_root() {
        $root = preg_replace('#/license$#i', '', $this->get_api_base());

        return untrailingslashit($root) . '/trial';
    }

    /**
     * @return true|WP_Error
     */
    public function start_trial() {
        $response = $this->request_trial_api('start');
        if (is_wp_error($response)) {
            return $response;
        }

        $this->save_trial_data($response);
        delete_transient(self::TRANSIENT_PREFIX . 'status');

        return true;
    }

    /**
     * Revalidate trial against license server.
     *
     * @return true|WP_Error
     */
    public function check_trial() {
        $response = $this->request_trial_api('check');
        if (is_wp_error($response)) {
            if ($response->get_error_code() === 'license_invalid' || $response->get_error_code() === 'license_expired') {
                $this->clear_trial_license();
            }
            return $response;
        }

        $this->save_trial_data($response);

        return true;
    }

    /**
     * @return void
     */
    public function maybe_revalidate_trial() {
        if (!is_admin()
            || !function_exists('maca_menulist_user_can_manage_plugin')
            || !maca_menulist_user_can_manage_plugin()) {
            return;
        }

        $data = $this->get_stored_license();
        if (!$this->is_trial_license($data)) {
            return;
        }

        $checked = !empty($data['checked_at']) ? (int) strtotime($data['checked_at'] . ' UTC') : 0;
        $interval = (int) apply_filters('maca_menulist_trial_revalidate_interval', 12 * HOUR_IN_SECONDS);
        if ($checked > 0 && (time() - $checked) < $interval) {
            return;
        }

        $this->check_trial();
    }

    /**
     * @return array<string, mixed>
     */
    public function get_stored_license() {
        $data = get_option(self::OPTION_KEY, array());
        return is_array($data) ? $data : array();
    }

    /**
     * @return string
     */
    public function get_subscribe_url() {
        $url = apply_filters(
            'maca_menulist_stripe_checkout_url',
            'https://maca.se/restu-pro/'
        );

        $args = array(
            'site_url'     => rawurlencode($this->get_public_site_url()),
            'sfls_product' => 'maca_restu_pro',
        );

        if (function_exists('maca_menulist_pro_pricing_lifetime_enabled')
            && !maca_menulist_pro_pricing_lifetime_enabled()
        ) {
            $args['sfls_plan'] = 'annual';
        }

        return add_query_arg($args, $url);
    }

    /**
     * Configured license API root (may be menulist or downlist namespace).
     *
     * @return string URL ending with /license (no action segment).
     */
    public function get_api_base() {
        $default = 'https://maca.se/wp-json/maca-menulist/v1/license';

        return $this->normalize_license_api_root(
            apply_filters('maca_menulist_license_api_url', $default)
        );
    }

    /**
     * @param string $url Filtered API URL from wp-config.
     * @return string Normalized .../license path without /activate suffix.
     */
    private function normalize_license_api_root($url) {
        $url = untrailingslashit(trim((string) $url));
        $url = preg_replace('#/(activate|deactivate|check)$#i', '', $url);

        if (!preg_match('#/license$#i', $url)) {
            $url = trailingslashit($url) . 'license';
            $url = untrailingslashit($url);
        }

        return $url;
    }

    /**
     * @param string $license_key License key.
     * @return true|WP_Error
     */
    public function activate($license_key) {
        $license_key = $this->normalize_key($license_key);
        if ($license_key === '') {
            return new WP_Error('empty_key', __('Please enter a license key.', 'maca-restu-pro'));
        }

        $response = $this->request_api('activate', $license_key);
        if (is_wp_error($response)) {
            return $response;
        }

        $this->save_license_data($response, $license_key);
        delete_transient(self::TRANSIENT_PREFIX . 'status');
        return true;
    }

    /**
     * @return true|WP_Error
     */
    public function deactivate() {
        $data = $this->get_stored_license();
        $key  = isset($data['license_key']) ? $data['license_key'] : '';

        if ($this->is_trial_license($data)) {
            $this->clear_trial_license();
            return true;
        }

        if ($key !== '') {
            $this->request_api('deactivate', $key);
        }

        delete_option(self::OPTION_KEY);
        delete_transient(self::TRANSIENT_PREFIX . 'status');
        return true;
    }

    /**
     * @return void
     */
    public function cron_validate() {
        $data = $this->get_stored_license();

        if ($this->is_trial_license($data)) {
            $this->check_trial();
            return;
        }

        if (empty($data['license_key'])) {
            return;
        }

        $key = $data['license_key'];

        $response = $this->request_api('check', $key);
        if (is_wp_error($response)) {
            return;
        }

        $this->save_license_data($response, $key);
    }

    /**
     * Map internal action to SFLS endpoint segment.
     *
     * @param string $action activate|validate|deactivate|check.
     * @return string
     */
    private function map_action($action) {
        if ($action === 'validate') {
            return 'check';
        }
        return $action;
    }

    /**
     * URLs to try (menulist SFLS, legacy single /license, downlist fallback).
     *
     * @param string $endpoint activate|check|deactivate.
     * @return string[]
     */
    private function get_license_api_urls($endpoint) {
        $root = $this->get_api_base();

        $urls = array(
            $root . '/' . $endpoint,
            $root,
        );

        $downlist_root = $this->normalize_license_api_root(
            apply_filters(
                'maca_menulist_license_downlist_api_url',
                'https://maca.se/wp-json/maca-downlist/v1/license'
            )
        );

        $urls[] = $downlist_root . '/' . $endpoint;

        return array_values(array_unique($urls));
    }

    /**
     * @param string $url Request URL.
     * @return bool
     */
    private function is_legacy_license_url($url) {
        return (bool) preg_match('#/license$#i', untrailingslashit($url));
    }

    /**
     * @param string $action activate|validate|deactivate|check.
     * @param string $license_key License key.
     * @return array<string, mixed>|WP_Error
     */
    private function request_api($action, $license_key) {
        $endpoint   = $this->map_action($action);
        $body_base  = array(
            'license_key' => $license_key,
            'site_url'    => $this->get_public_site_url(),
            'product'     => 'maca_restu_pro',
        );
        $last_error  = null;
        $last_code   = 0;
        $route_found = false;

        foreach ($this->get_license_api_urls($endpoint) as $url) {
            $body = $body_base;
            if ($this->is_legacy_license_url($url)) {
                $body['action'] = $endpoint;
            }

            $response = wp_remote_post(
                $url,
                array(
                    'timeout' => 15,
                    'headers' => array(
                        'Content-Type' => 'application/json; charset=utf-8',
                        'Accept'       => 'application/json',
                    ),
                    'body'    => wp_json_encode($body),
                )
            );

            if (is_wp_error($response)) {
                $last_error = $response;
                continue;
            }

            $code = (int) wp_remote_retrieve_response_code($response);
            $raw  = wp_remote_retrieve_body($response);
            $json = json_decode($raw, true);
            $last_code = $code;

            if ($code === 404 || (is_array($json) && isset($json['code']) && $json['code'] === 'rest_no_route')) {
                continue;
            }

            $route_found = true;

            if ($code >= 200 && $code < 300 && is_array($json)) {
                $parsed = $this->parse_sfls_response($json);
                if (!is_wp_error($parsed)) {
                    return $parsed;
                }
                $last_error = $parsed;
                continue;
            }

            $message = __('Invalid or expired license.', 'maca-restu-pro');
            if (is_array($json) && !empty($json['message'])) {
                $message = sanitize_text_field($json['message']);
            }
            $last_error = new WP_Error('license_invalid', $message, array('status' => $code));
        }

        if (!$route_found) {
            return new WP_Error(
                'license_api_missing',
                __('License API not found on the server. Contact Maca Development or update maca Licensserver on maca.se.', 'maca-restu-pro')
            );
        }

        if (is_wp_error($last_error)) {
            return $last_error;
        }

        return new WP_Error(
            'api_unreachable',
            __('Could not reach the license server. Try again later.', 'maca-restu-pro')
        );
    }

    /**
     * Normalize maca.se SFLS JSON to plugin storage format.
     *
     * @param array<string, mixed> $json SFLS response.
     * @return array<string, mixed>|WP_Error
     */
    private function parse_sfls_response($json) {
        $is_sfls = !empty($json['success']) && isset($json['status']) && $json['status'] === 'valid';
        $is_legacy = isset($json['status']) && $json['status'] === 'active';

        if (!$is_sfls && !$is_legacy) {
            $message = !empty($json['message'])
                ? sanitize_text_field($json['message'])
                : __('Invalid or expired license.', 'maca-restu-pro');
            $code = (isset($json['status']) && $json['status'] === 'expired') ? 'license_expired' : 'license_invalid';
            return new WP_Error($code, $message);
        }

        $expiry = $this->response_expiry_fields($json);
        $expires_at_ts = $expiry['expires_at_ts'];
        $days_remaining = null;

        if (isset($json['days_remaining']) && $json['days_remaining'] !== null && $json['days_remaining'] !== '') {
            $days_remaining = (int) $json['days_remaining'];
        } elseif ($expires_at_ts) {
            $days_remaining = $this->get_calendar_days_until_expiry($expires_at_ts);
        }

        return array(
            'status'         => 'active',
            'plan'           => isset($json['plan']) ? sanitize_text_field($json['plan']) : '',
            'expires'        => $expiry['expires'],
            'expires_at_ts'  => $expires_at_ts,
            'days_remaining' => $days_remaining,
            'customer_email' => isset($json['customer_email']) ? sanitize_email($json['customer_email']) : '',
            'is_comp'        => $this->response_is_complimentary($json),
            'is_trial'       => !empty($json['trial']) || (isset($json['plan']) && sanitize_key($json['plan']) === 'trial'),
        );
    }

    /**
     * @param string $action start|check.
     * @return array<string, mixed>|WP_Error
     */
    private function request_trial_api($action) {
        $url = $this->get_trial_api_root() . '/' . $action;
        $body = array(
            'site_url'          => $this->get_public_site_url(),
            'product'           => 'maca_restu_pro',
            'site_fingerprint'  => $this->get_site_fingerprint(),
            'site_title'        => get_bloginfo('name'),
            'wp_version'        => get_bloginfo('version'),
            'plugin_version'    => defined('MACA_MENULIST_VERSION') ? MACA_MENULIST_VERSION : '',
        );

        $response = wp_remote_post(
            $url,
            array(
                'timeout' => 15,
                'headers' => array(
                    'Content-Type' => 'application/json; charset=utf-8',
                    'Accept'       => 'application/json',
                ),
                'body'    => wp_json_encode($body),
            )
        );

        if (is_wp_error($response)) {
            return $response;
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $json = json_decode(wp_remote_retrieve_body($response), true);

        if ($code >= 200 && $code < 300 && is_array($json)) {
            $parsed = $this->parse_sfls_response($json);
            if (!is_wp_error($parsed)) {
                return $parsed;
            }
            return $parsed;
        }

        $message = __('Could not start free trial.', 'maca-restu-pro');
        if (is_array($json) && !empty($json['message'])) {
            $message = sanitize_text_field($json['message']);
        }

        return new WP_Error('trial_failed', $message);
    }

    /**
     * @param array<string, mixed> $api_response Normalized trial response.
     * @return void
     */
    private function save_trial_data($api_response) {
        if (is_wp_error($api_response)) {
            return;
        }

        $api_response['is_trial'] = true;
        $this->save_license_data($api_response, '');
    }

    /**
     * Remove expired or revoked trial state locally.
     *
     * @return void
     */
    private function clear_trial_license() {
        delete_option(self::OPTION_KEY);
        delete_transient(self::TRANSIENT_PREFIX . 'status');
    }

    /**
     * Build admin notice context for an expiring trial.
     *
     * @return array<string, mixed>|null
     */
    public function get_trial_expiry_notice_context() {
        $data = $this->get_stored_license();
        if (!$this->is_trial_license($data) || empty($data['status']) || $data['status'] !== 'active') {
            return null;
        }

        $expires_ts = $this->get_license_expiry_timestamp($data);
        if ($expires_ts <= 0) {
            return null;
        }

        $days_left = $this->get_calendar_days_until_expiry($expires_ts);
        if ($days_left < 0 || $days_left > 7) {
            return null;
        }

        return array(
            'days_left' => $days_left,
            'expires_label' => date_i18n(get_option('date_format'), $expires_ts),
            'subscribe_url' => $this->get_subscribe_url(),
            'subscription_url' => admin_url('admin.php?page=maca-restu-subscription'),
        );
    }

    /**
     * @return void
     */
    public function render_trial_expiry_notice() {
        if (!is_admin()
            || !function_exists('maca_menulist_user_can_manage_plugin')
            || !maca_menulist_user_can_manage_plugin()) {
            return;
        }

        $context = $this->get_trial_expiry_notice_context();
        if ($context === null) {
            return;
        }

        $days_left = (int) $context['days_left'];
        if ($days_left === 0) {
            $message = sprintf(
                /* translators: %s: expiration date */
                __('Din gratis Pro-testperiod går ut idag (%s).', 'maca-restu-pro'),
                $context['expires_label']
            );
        } elseif ($days_left === 1) {
            $message = sprintf(
                /* translators: %s: expiration date */
                __('Din gratis Pro-testperiod går ut om 1 dag (%s).', 'maca-restu-pro'),
                $context['expires_label']
            );
        } else {
            $message = sprintf(
                /* translators: 1: days left, 2: expiration date */
                __('Din gratis Pro-testperiod går ut om %1$d dagar (%2$s).', 'maca-restu-pro'),
                $days_left,
                $context['expires_label']
            );
        }

        echo '<div class="notice notice-warning maca-trial-expiry-notice is-dismissible">';
        echo '<p>' . esc_html($message) . '</p>';
        echo '<p><a class="button button-primary" href="' . esc_url($context['subscribe_url']) . '" target="_blank" rel="noopener noreferrer">';
        esc_html_e('Prenumerera på maca.se', 'maca-restu-pro');
        echo '</a> ';
        echo '<a class="button" href="' . esc_url($context['subscription_url']) . '">';
        esc_html_e('Hantera prenumeration', 'maca-restu-pro');
        echo '</a></p></div>';
    }

    /**
     * @param array<string, mixed> $api_response Normalized response.
     * @param string               $license_key License key.
     * @return void
     */
    private function save_license_data($api_response, $license_key) {
        if (is_wp_error($api_response)) {
            return;
        }

        $status = isset($api_response['status']) ? sanitize_key($api_response['status']) : 'inactive';
        $expires_at_ts = isset($api_response['expires_at_ts']) && $api_response['expires_at_ts'] !== null
            ? (int) $api_response['expires_at_ts']
            : null;
        $days_remaining = isset($api_response['days_remaining']) && $api_response['days_remaining'] !== null
            ? (int) $api_response['days_remaining']
            : null;

        if ($days_remaining === null && $expires_at_ts) {
            $days_remaining = $this->get_calendar_days_until_expiry($expires_at_ts);
        }

        $stored = array(
            'license_key'    => $this->normalize_key($license_key),
            'status'         => $status,
            'plan'           => isset($api_response['plan']) ? sanitize_text_field($api_response['plan']) : '',
            'expires'        => isset($api_response['expires']) ? sanitize_text_field($api_response['expires']) : '',
            'expires_at_ts'  => $expires_at_ts,
            'days_remaining' => $days_remaining,
            'customer_email' => isset($api_response['customer_email']) ? sanitize_email($api_response['customer_email']) : '',
            'is_comp'        => !empty($api_response['is_comp']) || $this->is_complimentary_license(
                array(
                    'license_key' => $license_key,
                    'plan' => isset($api_response['plan']) ? $api_response['plan'] : '',
                    'is_comp' => !empty($api_response['is_comp']),
                )
            ),
            'is_trial'       => !empty($api_response['is_trial']) || $this->is_trial_license(
                array(
                    'plan' => isset($api_response['plan']) ? $api_response['plan'] : '',
                    'is_trial' => !empty($api_response['is_trial']),
                )
            ),
            'checked_at'     => gmdate('Y-m-d H:i:s'),
        );

        update_option(self::OPTION_KEY, $stored, false);
    }

    /**
     * Show warning banner when complimentary license is nearing expiry.
     *
     * @return void
     */
    public function render_comp_expiry_notice() {
        if (!is_admin()
            || !function_exists('maca_menulist_user_can_manage_plugin')
            || !maca_menulist_user_can_manage_plugin()) {
            return;
        }

        $context = $this->get_comp_expiry_notice_context();
        if ($context === null) {
            return;
        }

        $days_left = (int) $context['days_left'];
        $plugin_name = (string) $context['plugin_name'];
        $expires_label = (string) $context['expires_label'];

        if ($days_left === 0) {
            $message = sprintf(
                /* translators: %s: expiration date */
                __('Din komplicens går ut idag (%s).', 'maca-restu-pro'),
                $expires_label
            );
        } elseif ($days_left === 1) {
            $message = sprintf(
                /* translators: %s: expiration date */
                __('Din komplicens går ut om 1 dag (%s).', 'maca-restu-pro'),
                $expires_label
            );
        } else {
            $message = sprintf(
                /* translators: 1: days left, 2: expiration date */
                __('Din komplicens går ut om %1$d dagar (%2$s).', 'maca-restu-pro'),
                $days_left,
                $expires_label
            );
        }

        echo '<div class="notice notice-warning maca-comp-license-expiry-notice is-dismissible">';
        echo '<p><strong>' . esc_html($plugin_name) . '</strong> — ';
        echo '<span class="maca-comp-license-expiry-countdown">';
        echo esc_html($message);
        echo '</span></p>';
        echo '<p class="maca-comp-license-expiry-actions">';
        echo '<a class="button button-primary" href="' . esc_url($context['subscribe_url']) . '" target="_blank" rel="noopener noreferrer">';
        esc_html_e('Prenumerera på maca.se', 'maca-restu-pro');
        echo '</a> ';
        echo '<a class="button" href="' . esc_url($context['subscription_url']) . '">';
        esc_html_e('Hantera licens', 'maca-restu-pro');
        echo '</a></p></div>';
    }

    /**
     * @param string $key License key.
     * @return string
     */
    private function normalize_key($key) {
        return strtoupper(trim(sanitize_text_field($key)));
    }

    /**
     * @return void
     */
    public function ajax_activate() {
        check_ajax_referer('maca_menulist_license', 'nonce');

        if (!maca_menulist_user_can_manage_secrets()) {
            wp_send_json_error(array('message' => __('Permission denied.', 'maca-restu-pro')), 403);
        }

        $key    = isset($_POST['license_key']) ? wp_unslash($_POST['license_key']) : '';
        $result = $this->activate($key);

        if (is_wp_error($result)) {
            wp_send_json_error(array('message' => $result->get_error_message()));
        }

        wp_send_json_success(array('message' => __('License activated.', 'maca-restu-pro')));
    }

    /**
     * @return void
     */
    public function ajax_deactivate() {
        check_ajax_referer('maca_menulist_license', 'nonce');

        if (!maca_menulist_user_can_manage_secrets()) {
            wp_send_json_error(array('message' => __('Permission denied.', 'maca-restu-pro')), 403);
        }

        $this->deactivate();
        wp_send_json_success(array('message' => __('License removed from this site.', 'maca-restu-pro')));
    }

    /**
     * @return void
     */
    public function ajax_start_trial() {
        check_ajax_referer('maca_menulist_license', 'nonce');

        if (!maca_menulist_user_can_manage_secrets()) {
            wp_send_json_error(array('message' => __('Permission denied.', 'maca-restu-pro')), 403);
        }

        if (maca_menulist_pro_is_active()) {
            wp_send_json_error(array('message' => __('Pro is already active on this site.', 'maca-restu-pro')));
        }

        $config = $this->get_trial_config(true);
        if (empty($config['enabled'])) {
            wp_send_json_error(array('message' => __('Free Pro trial is not available right now.', 'maca-restu-pro')));
        }

        $result = $this->start_trial();
        if (is_wp_error($result)) {
            wp_send_json_error(array('message' => $result->get_error_message()));
        }

        wp_send_json_success(
            array(
                'message' => sprintf(
                    /* translators: %d: trial days */
                    __('Free Pro trial started (%d days).', 'maca-restu-pro'),
                    (int) $config['days']
                ),
            )
        );
    }
}

if (defined('MACA_MENULIST_LICENSE_API_URL') && MACA_MENULIST_LICENSE_API_URL) {
    add_filter(
        'maca_menulist_license_api_url',
        static function () {
            return MACA_MENULIST_LICENSE_API_URL;
        }
    );
}

if (defined('MACA_MENULIST_LICENSE_DOWNLIST_API_URL') && MACA_MENULIST_LICENSE_DOWNLIST_API_URL) {
    add_filter(
        'maca_menulist_license_downlist_api_url',
        static function () {
            return MACA_MENULIST_LICENSE_DOWNLIST_API_URL;
        }
    );
}
