<?php
/**
 * Focus Keywords Data Layer
 *
 * Handles multi-keyword storage with backward compatibility for the
 * legacy single-keyword meta key. All keyword read/write operations
 * should go through this class.
 *
 * Storage format (post meta `_prorank_seo_focus_keywords`):
 *   PHP array serialized by WordPress: [["keyword" => "seo tools", "primary" => true], ["keyword" => "seo software", "primary" => false]]
 *
 * Backward compatibility:
 *   - `_prorank_seo_focus_keyword` (singular) is kept in sync as the primary keyword string
 *   - `_prorank_focus_keyword` (legacy) is auto-migrated on first read
 *   - Consumers that only read the singular key continue to work unchanged
 *
 * @package ProRank\SEO\Core
 * @since   1.1.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Core;

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

class FocusKeywords {

    /** New plural meta key for the keyword array. */
    public const META_KEY = '_prorank_seo_focus_keywords';

    /** Current singular meta key (kept in sync as primary keyword). */
    public const SINGULAR_META_KEY = '_prorank_seo_focus_keyword';

    /** Legacy singular meta key (auto-migrated). */
    public const LEGACY_META_KEY = '_prorank_focus_keyword';

    /** Per-keyword max length in characters. */
    public const MAX_KEYWORD_LENGTH = 120;

    /** Tier limits: tier => max keyword count. */
    private const TIER_LIMITS = [
        'free'     => 3,
        'pro'      => 10,
        'business' => 10,
        'agency'   => 10,
    ];

    /**
     * Get all focus keywords for a post.
     *
     * Reads the plural key first. If empty, falls back to the singular key
     * (and legacy key), auto-migrating into the plural format.
     *
     * @param int $post_id Post ID.
     * @return array<int, array{keyword: string, primary: bool}>
     */
    public static function get( int $post_id ): array {
        $raw = get_post_meta( $post_id, self::META_KEY, true );

        if ( is_array( $raw ) && ! empty( $raw ) ) {
            return self::normalize( $raw );
        }

        // Decode JSON string if stored that way.
        if ( is_string( $raw ) && $raw !== '' ) {
            $decoded = json_decode( $raw, true );
            if ( is_array( $decoded ) && ! empty( $decoded ) ) {
                return self::normalize( $decoded );
            }
        }

        // Fall back to singular key.
        $singular = (string) get_post_meta( $post_id, self::SINGULAR_META_KEY, true );

        if ( $singular === '' ) {
            // Try legacy key.
            $legacy = (string) get_post_meta( $post_id, self::LEGACY_META_KEY, true );
            if ( $legacy !== '' ) {
                $singular = $legacy;
                // Migrate legacy → singular.
                update_post_meta( $post_id, self::SINGULAR_META_KEY, $legacy );
            }
        }

        if ( $singular === '' ) {
            return [];
        }

        // Auto-migrate singular → plural format.
        $keywords = [ [ 'keyword' => $singular, 'primary' => true ] ];
        update_post_meta( $post_id, self::META_KEY, $keywords );

        return $keywords;
    }

    /**
     * Get only the primary (first) keyword string for a post.
     *
     * This is the backward-compatible accessor that returns the same value
     * the old singular key would have returned.
     *
     * @param int $post_id Post ID.
     * @return string Primary keyword or empty string.
     */
    public static function get_primary( int $post_id ): string {
        $keywords = self::get( $post_id );

        foreach ( $keywords as $kw ) {
            if ( ! empty( $kw['primary'] ) ) {
                return $kw['keyword'];
            }
        }

        // If no explicit primary, return first.
        return ! empty( $keywords ) ? $keywords[0]['keyword'] : '';
    }

    /**
     * Save focus keywords for a post.
     *
     * Enforces tier limits and keeps the singular meta key in sync.
     *
     * @param int   $post_id  Post ID.
     * @param array $keywords Array of keyword entries.
     * @param string|null $tier Override tier (null = auto-detect from license).
     * @return array{saved: array, primary: string, truncated: bool}
     */
    public static function save( int $post_id, array $keywords, ?string $tier = null ): array {
        $keywords  = self::sanitize( $keywords );
        $limit     = self::get_limit( $tier );
        $truncated = count( $keywords ) > $limit;
        $keywords  = array_slice( $keywords, 0, $limit );

        // Ensure exactly one primary.
        $keywords = self::ensure_primary( $keywords );

        // Persist plural key.
        update_post_meta( $post_id, self::META_KEY, $keywords );

        // Keep singular key in sync (primary keyword).
        $primary = self::extract_primary( $keywords );
        update_post_meta( $post_id, self::SINGULAR_META_KEY, $primary );

        return [
            'saved'     => $keywords,
            'primary'   => $primary,
            'truncated' => $truncated,
        ];
    }

    /**
     * Get the keyword limit for a tier.
     *
     * @param string|null $tier Tier slug. Null = auto-detect.
     * @return int
     */
    public static function get_limit( ?string $tier = null ): int {
        if ( $tier === null ) {
            $tier = self::detect_tier();
        }

        // Normalize legacy alias.
        if ( $tier === 'core' ) {
            $tier = 'pro';
        }

        return self::TIER_LIMITS[ $tier ] ?? self::TIER_LIMITS['free'];
    }

    /**
     * Sanitize a raw keywords array from user input.
     *
     * Accepts both the new object format and a plain string (single keyword).
     *
     * @param mixed $input Raw input.
     * @return array<int, array{keyword: string, primary: bool}>
     */
    public static function sanitize( $input ): array {
        // Accept a plain string as a single primary keyword.
        if ( is_string( $input ) ) {
            $input = trim( $input );
            if ( $input === '' ) {
                return [];
            }
            return [ [ 'keyword' => self::sanitize_keyword( $input ), 'primary' => true ] ];
        }

        if ( ! is_array( $input ) ) {
            return [];
        }

        $result = [];
        $seen   = [];

        foreach ( $input as $entry ) {
            if ( is_string( $entry ) ) {
                $entry = [ 'keyword' => $entry, 'primary' => false ];
            }

            if ( ! is_array( $entry ) || empty( $entry['keyword'] ) ) {
                continue;
            }

            $kw = self::sanitize_keyword( (string) $entry['keyword'] );
            if ( $kw === '' ) {
                continue;
            }

            // Deduplicate (case-insensitive).
            $lower = mb_strtolower( $kw, 'UTF-8' );
            if ( isset( $seen[ $lower ] ) ) {
                continue;
            }
            $seen[ $lower ] = true;

            $result[] = [
                'keyword' => $kw,
                'primary' => ! empty( $entry['primary'] ),
            ];
        }

        return $result;
    }

    /**
     * Normalize stored keyword data.
     *
     * @param array $raw Raw array from post meta.
     * @return array<int, array{keyword: string, primary: bool}>
     */
    private static function normalize( array $raw ): array {
        $result = [];
        foreach ( $raw as $entry ) {
            if ( is_string( $entry ) ) {
                $entry = [ 'keyword' => $entry, 'primary' => false ];
            }
            if ( ! is_array( $entry ) || empty( $entry['keyword'] ) ) {
                continue;
            }
            $result[] = [
                'keyword' => (string) $entry['keyword'],
                'primary' => ! empty( $entry['primary'] ),
            ];
        }

        return self::ensure_primary( $result );
    }

    /**
     * Ensure exactly one keyword is marked primary.
     *
     * @param array $keywords Keyword array.
     * @return array
     */
    private static function ensure_primary( array $keywords ): array {
        if ( empty( $keywords ) ) {
            return [];
        }

        // Enforce single-primary: first marked primary wins; demote the rest.
        $primary_found = false;
        foreach ( $keywords as &$kw ) {
            if ( ! empty( $kw['primary'] ) ) {
                if ( $primary_found ) {
                    $kw['primary'] = false;
                }
                $primary_found = true;
            }
        }
        unset( $kw );

        // If none marked, first becomes primary.
        if ( ! $primary_found ) {
            $keywords[0]['primary'] = true;
        }

        return $keywords;
    }

    /**
     * Extract the primary keyword string.
     *
     * @param array $keywords Keyword array.
     * @return string
     */
    private static function extract_primary( array $keywords ): string {
        foreach ( $keywords as $kw ) {
            if ( ! empty( $kw['primary'] ) ) {
                return $kw['keyword'];
            }
        }
        return ! empty( $keywords ) ? $keywords[0]['keyword'] : '';
    }

    /**
     * Sanitize a single keyword string.
     *
     * @param string $keyword Raw keyword.
     * @return string
     */
    private static function sanitize_keyword( string $keyword ): string {
        $keyword = sanitize_text_field( $keyword );

        if ( function_exists( 'mb_substr' ) ) {
            $keyword = mb_substr( $keyword, 0, self::MAX_KEYWORD_LENGTH, 'UTF-8' );
        } else {
            $keyword = substr( $keyword, 0, self::MAX_KEYWORD_LENGTH );
        }

        return $keyword;
    }

    /**
     * Detect the current license tier.
     *
     * @return string
     */
    private static function detect_tier(): string {
        return 'free';
    }
}
