<?php

namespace WPSN\Modules\Stability;

use WPSN\Core\Container;
use WPSN\Domain\SeoDocumentContext;

defined('ABSPATH') || exit;

final class HeadSnapshotService
{
    public const OPTION_BASELINE_ID = 'wpsn_head_baseline_snapshot_id';

    private Container $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    public function getBaselineId(): int
    {
        return (int) get_option(self::OPTION_BASELINE_ID, 0);
    }

    public function setBaselineId(int $id): void
    {
        $id = (int) $id;
        if ($id < 0) {
            $id = 0;
        }
        update_option(self::OPTION_BASELINE_ID, $id, false);
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public function buildDefaultCases(): array
    {
        $cases = [];

        // Home.
        $cases[] = [
            'key' => 'home',
            'label' => __('Home', 'wp-seo-next'),
            'context' => [
                'type' => SeoDocumentContext::TYPE_HOME,
                'object_id' => null,
                'object_sub_type' => null,
                'url' => home_url('/'),
                'paged' => 1,
            ],
        ];

        // Post (fallback to page).
        $postId = 0;
        $postType = '';
        $p = get_posts([
            'post_type' => 'post',
            'post_status' => 'publish',
            'numberposts' => 1,
            'orderby' => 'date',
            'order' => 'DESC',
            'fields' => 'ids',
        ]);
        if (is_array($p) && !empty($p[0])) {
            $postId = (int) $p[0];
            $postType = 'post';
        } else {
            $p = get_posts([
                'post_type' => 'page',
                'post_status' => 'publish',
                'numberposts' => 1,
                'orderby' => 'date',
                'order' => 'DESC',
                'fields' => 'ids',
            ]);
            if (is_array($p) && !empty($p[0])) {
                $postId = (int) $p[0];
                $postType = 'page';
            }
        }

        if ($postId > 0) {
            $url = get_permalink($postId);
            if (!is_string($url) || $url === '') {
                $url = home_url('/');
            }

            $cases[] = [
                'key' => 'post_' . $postId,
                'label' => sprintf(__('Post #%d', 'wp-seo-next'), $postId),
                'context' => [
                    'type' => SeoDocumentContext::TYPE_SINGULAR,
                    'object_id' => $postId,
                    'object_sub_type' => $postType !== '' ? $postType : (get_post_type($postId) ?: null),
                    'url' => (string) $url,
                    'paged' => 1,
                ],
            ];
        }

        // Term (category).
        $termId = 0;
        $termTax = 'category';
        $terms = get_terms([
            'taxonomy' => 'category',
            'hide_empty' => false,
            'number' => 1,
        ]);
        if (is_array($terms) && !empty($terms[0]) && $terms[0] instanceof \WP_Term) {
            $termId = (int) $terms[0]->term_id;
        }

        if ($termId > 0) {
            $term = get_term($termId, $termTax);
            if ($term instanceof \WP_Term) {
                $url = get_term_link($term);
                if (is_wp_error($url)) {
                    $url = home_url('/');
                }

                $cases[] = [
                    'key' => 'term_' . $termId,
                    'label' => sprintf(__('Term #%d (%s)', 'wp-seo-next'), $termId, $termTax),
                    'context' => [
                        'type' => SeoDocumentContext::TYPE_TERM,
                        'object_id' => $termId,
                        'object_sub_type' => $termTax,
                        'url' => (string) $url,
                        'paged' => 1,
                    ],
                ];
            }
        }

        // WooCommerce product (optional).
        if (class_exists('WooCommerce') && function_exists('wc_get_products')) {
            $products = wc_get_products([
                'status' => 'publish',
                'limit' => 1,
                'orderby' => 'date',
                'order' => 'DESC',
                'return' => 'ids',
            ]);
            if (is_array($products) && !empty($products[0])) {
                $pid = (int) $products[0];
                if ($pid > 0) {
                    $url = get_permalink($pid);
                    if (!is_string($url) || $url === '') {
                        $url = home_url('/');
                    }
                    $cases[] = [
                        'key' => 'product_' . $pid,
                        'label' => sprintf(__('Product #%d', 'wp-seo-next'), $pid),
                        'context' => [
                            'type' => SeoDocumentContext::TYPE_SINGULAR,
                            'object_id' => $pid,
                            'object_sub_type' => 'product',
                            'url' => (string) $url,
                            'paged' => 1,
                        ],
                    ];
                }
            }
        }

        return $cases;
    }

    /**
     * Build a head snapshot payload for the provided cases.
     *
     * @param array<int,array<string,mixed>> $cases
     * @return array<string,mixed>
     */
    public function buildSnapshotPayload(array $cases): array
    {
        /** @var \WPSN\SEO\PublicHeadBuilder $hb */
        $hb = $this->container->get('public_head_builder');

        $outCases = [];

        foreach ($cases as $case) {
            if (!is_array($case)) {
                continue;
            }

            $key = isset($case['key']) ? sanitize_key((string) $case['key']) : '';
            $label = isset($case['label']) ? sanitize_text_field((string) $case['label']) : $key;

            $c = isset($case['context']) && is_array($case['context']) ? $case['context'] : [];
            $type = isset($c['type']) ? (string) $c['type'] : '';
            $oid = isset($c['object_id']) ? $c['object_id'] : null;
            $ost = isset($c['object_sub_type']) ? $c['object_sub_type'] : null;
            $url = isset($c['url']) ? (string) $c['url'] : '';
            $paged = isset($c['paged']) ? (int) $c['paged'] : 1;

            if ($type === '' || $url === '') {
                continue;
            }

            $ctx = new SeoDocumentContext(
                $type,
                $oid !== null ? (int) $oid : null,
                $ost !== null && $ost !== '' ? (string) $ost : null,
                $url,
                $paged > 0 ? $paged : 1
            );

            $built = $hb->build($ctx);

            // Don't serialize the meta object.
            if (isset($built['__meta'])) {
                unset($built['__meta']);
            }

            $headHtml = isset($built['head']['html']) ? (string) $built['head']['html'] : '';
            $headTags = isset($built['head']['tags']) && is_array($built['head']['tags']) ? $built['head']['tags'] : null;
            $schema = isset($built['schema_graph']) && is_array($built['schema_graph']) ? $built['schema_graph'] : null;
            $schemaHash = '';
            if (is_array($schema)) {
                $encoded = wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
                if (is_string($encoded)) {
                    $schemaHash = hash('sha256', $encoded);
                }
            }

            $schemaTypes = is_array($schema) ? $this->extractSchemaTypes($schema) : [];
            $struct = is_array($headTags) ? $this->normalizeHeadTags($headTags, $schemaHash, $schemaTypes) : [
                'title' => '',
                'meta' => [],
                'link' => [],
                'schema_hash' => $schemaHash,
                'schema_types' => $schemaTypes,
            ];
            $structHash = $this->hashStable($struct);

            $outCases[] = [
                'key' => $key,
                'label' => $label,
                'context' => [
                    'type' => $type,
                    'object_id' => $oid !== null ? (int) $oid : null,
                    'object_sub_type' => $ost !== null && $ost !== '' ? (string) $ost : null,
                    'url' => $url,
                    'paged' => $paged > 0 ? $paged : 1,
                ],
                'modules' => isset($built['modules']) && is_array($built['modules']) ? $built['modules'] : [],
                'resolved' => isset($built['resolved']) && is_array($built['resolved']) ? $built['resolved'] : [],
                // Structured head (stable) is used for precise diffs and update regression checks.
                // NOTE: We intentionally do NOT store full JSON-LD scripts here; schema_hash is enough.
                'head_struct' => $struct,
                'struct_hash' => $structHash,
                'head_hash' => $headHtml !== '' ? hash('sha256', $headHtml) : '',
                'head_html' => $headHtml,
                'schema_hash' => $schemaHash,
            ];
        }

        return [
            'site_url' => home_url('/'),
            'created_at' => gmdate('c'),
            'plugin_version' => (string) (defined('WPSN_VERSION') ? WPSN_VERSION : ''),
            'cases' => $outCases,
        ];
    }

    /**
     * Normalize structured head tags to a stable diff-friendly payload.
     *
     * @param array<string,mixed> $headTags
     * @param string $schemaHash
     * @param array<int,string> $schemaTypes
     * @return array<string,mixed>
     */
    private function normalizeHeadTags(array $headTags, string $schemaHash, array $schemaTypes): array
    {
        $title = isset($headTags['title']) && is_string($headTags['title']) ? trim($headTags['title']) : '';
        $meta = isset($headTags['meta']) && is_array($headTags['meta']) ? $headTags['meta'] : [];
        $link = isset($headTags['link']) && is_array($headTags['link']) ? $headTags['link'] : [];

        $metaMap = $this->tagsListToMap($meta, 'meta');
        $linkMap = $this->tagsListToMap($link, 'link');

        return [
            'title' => $title,
            'meta' => $metaMap,
            'link' => $linkMap,
            'schema_hash' => $schemaHash,
            'schema_types' => $schemaTypes,
        ];
    }

    /**
     * Convert a list of meta/link tags into a stable key-value map.
     *
     * - meta keys are `name:robots` or `property:og:title`
     * - link keys are `rel:canonical`
     *
     * Multiple values for the same key are stored as a sorted list.
     *
     * @param array<int,mixed> $list
     * @param string $type
     * @return array<string,mixed>
     */
    private function tagsListToMap(array $list, string $type): array
    {
        $out = [];

        foreach ($list as $item) {
            if (!is_array($item)) {
                continue;
            }

            $key = '';
            $val = '';

            if ($type === 'meta') {
                if (isset($item['name']) && is_string($item['name']) && $item['name'] !== '') {
                    $key = 'name:' . (string) $item['name'];
                } elseif (isset($item['property']) && is_string($item['property']) && $item['property'] !== '') {
                    $key = 'property:' . (string) $item['property'];
                }
                $val = isset($item['content']) && is_string($item['content']) ? (string) $item['content'] : '';
            } elseif ($type === 'link') {
                $key = (isset($item['rel']) && is_string($item['rel']) && $item['rel'] !== '') ? 'rel:' . (string) $item['rel'] : '';
                $val = isset($item['href']) && is_string($item['href']) ? (string) $item['href'] : '';
            }

            $key = trim($key);
            if ($key === '' || $val === '') {
                continue;
            }

            if (!isset($out[$key])) {
                $out[$key] = $val;
                continue;
            }

            // Multiple values per key → store as sorted list.
            if (is_array($out[$key])) {
                $out[$key][] = $val;
            } else {
                $out[$key] = [$out[$key], $val];
            }
        }

        foreach ($out as $k => $v) {
            if (is_array($v)) {
                $vv = array_values(array_filter(array_map('strval', $v), static function ($x) {
                    return $x !== '';
                }));
                sort($vv);
                $out[$k] = $vv;
            }
        }

        ksort($out);

        return $out;
    }

    /**
     * Extract a stable list of schema types from a schema graph.
     *
     * @param array<string,mixed> $schema
     * @return array<int,string>
     */
    private function extractSchemaTypes(array $schema): array
    {
        $types = [];

        $graph = null;
        if (isset($schema['@graph']) && is_array($schema['@graph'])) {
            $graph = $schema['@graph'];
        } elseif (isset($schema[0]) && is_array($schema[0])) {
            // Some builders might return a list.
            $graph = $schema;
        }

        if (is_array($graph)) {
            foreach ($graph as $node) {
                if (!is_array($node)) {
                    continue;
                }
                if (!isset($node['@type'])) {
                    continue;
                }
                $t = $node['@type'];
                if (is_string($t) && $t !== '') {
                    $types[] = $t;
                } elseif (is_array($t)) {
                    foreach ($t as $tt) {
                        if (is_string($tt) && $tt !== '') {
                            $types[] = $tt;
                        }
                    }
                }
            }
        }

        $types = array_values(array_unique(array_filter(array_map('strval', $types), static function ($x) {
            return $x !== '';
        })));
        sort($types);
        return $types;
    }

    /**
     * Stable hash for structured data (sorts associative arrays).
     *
     * @param mixed $data
     */
    private function hashStable($data): string
    {
        $normalized = $this->stableNormalize($data);
        $json = wp_json_encode($normalized, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return is_string($json) && $json !== '' ? hash('sha256', $json) : '';
    }

    /**
     * @param mixed $data
     * @return mixed
     */
    private function stableNormalize($data)
    {
        if (is_array($data)) {
            $isAssoc = array_keys($data) !== range(0, count($data) - 1);
            if ($isAssoc) {
                $out = [];
                foreach ($data as $k => $v) {
                    $out[(string) $k] = $this->stableNormalize($v);
                }
                ksort($out);
                return $out;
            }
            $out = [];
            foreach ($data as $v) {
                $out[] = $this->stableNormalize($v);
            }
            // If list is purely scalar, sort to stabilize.
            $allScalar = true;
            foreach ($out as $v) {
                if (!(is_string($v) || is_int($v) || is_float($v) || is_bool($v) || $v === null)) {
                    $allScalar = false;
                    break;
                }
            }
            if ($allScalar) {
                sort($out);
            }
            return $out;
        }

        if (is_object($data)) {
            // Convert objects to arrays (best-effort).
            return $this->stableNormalize((array) $data);
        }

        return $data;
    }
}
