<?php
/**
 * SeoService — clean URL generation, slug management, and rewrite rules.
 *
 * Slugs are read directly from the authoritative base-table columns
 * (tc_products.slug / tc_categories.slug), filtered to live rows
 * (deleted_at IS NULL). There is no separate slug table.
 * Registers WordPress rewrite rules for:
 *   /shop/                                    → shop index
 *   /shop/{category-slug}/                    → category page
 *   /shop/{parent-slug}/{child-slug}/         → nested category
 *   /product/{slug}/                          → single product
 *
 * @package TitanCart\Services
 */

namespace TitanCart\Services;

defined( 'ABSPATH' ) || exit;

class SeoService {

    /** @var array|null Cached URL patterns from DB. */
    private static ?array $patterns = null;

    /** Default URL patterns — used when DB has no settings or invalid patterns. */
    private const DEFAULT_PRODUCT_PATTERN  = '/product/{slug}/';
    private const DEFAULT_CATEGORY_PATTERN = '/shop/{slug}/';

    /**
     * Tag URL prefix. Hardcoded to '/product-tag/' to avoid collision with
     * WordPress's built-in '/tag/' rewrite for the post_tag taxonomy. Mirrors
     * WooCommerce's '/product-tag/' convention. Could be made configurable
     * later if needed; if so, validate against a whitelist like product/category.
     */
    private const TAG_PREFIX = 'product-tag';

    /** Allowed product URL patterns (whitelist). */
    public const ALLOWED_PRODUCT_PATTERNS = [
        '/product/{slug}/',
        '/p/{slug}/',
        '/item/{slug}/',
        '/goods/{slug}/',
    ];

    /** Allowed category URL patterns (whitelist). */
    public const ALLOWED_CATEGORY_PATTERNS = [
        '/shop/{slug}/',
        '/category/{slug}/',
        '/catalog/{slug}/',
        '/c/{slug}/',
        '/browse/{slug}/',
    ];

    /* ──────────────────────────────────────────────────────────────────────────
     * URL pattern config — read from tc_settings, cached per request.
     * Only whitelisted patterns in ALLOWED_PRODUCT_PATTERNS / ALLOWED_CATEGORY_PATTERNS
     * are accepted. Invalid values fall back to defaults.
     * ────────────────────────────────────────────────────────────────────────── */

    /**
     * Get the configured URL patterns.
     * Returns [ 'product_prefix' => 'product', 'category_prefix' => 'shop', ... ]
     */
    public static function get_url_patterns(): array {
        if ( self::$patterns !== null ) {
            return self::$patterns;
        }

        $product_pattern  = self::DEFAULT_PRODUCT_PATTERN;
        $category_pattern = self::DEFAULT_CATEGORY_PATTERN;

        try {
            $db   = titancart_db();
            $rows = $db->getArray(
                "SELECT `key`, value FROM {$db->table('settings')}
                 WHERE section = 'seo' AND `key` IN ('product_url_pattern', 'category_url_pattern')"
            );
            foreach ( $rows as $r ) {
                if ( $r['key'] === 'product_url_pattern' && ! empty( $r['value'] ) ) {
                    $product_pattern = $r['value'];
                }
                if ( $r['key'] === 'category_url_pattern' && ! empty( $r['value'] ) ) {
                    $category_pattern = $r['value'];
                }
            }
        } catch ( \Throwable $e ) {
            // DB not ready yet — use defaults.
        }

        // Validate against whitelist — reject anything not in the allowed list.
        if ( ! in_array( $product_pattern, self::ALLOWED_PRODUCT_PATTERNS, true ) ) {
            $product_pattern = self::DEFAULT_PRODUCT_PATTERN;
        }
        if ( ! in_array( $category_pattern, self::ALLOWED_CATEGORY_PATTERNS, true ) ) {
            $category_pattern = self::DEFAULT_CATEGORY_PATTERN;
        }

        // Extract prefixes (guaranteed safe — all whitelisted patterns have static first segments).
        $prod_pfx = self::extract_prefix( $product_pattern );
        $cat_pfx  = self::extract_prefix( $category_pattern );

        // Ensure product and category prefixes don't collide.
        if ( $prod_pfx === $cat_pfx ) {
            $prod_pfx        = 'product';
            $product_pattern  = self::DEFAULT_PRODUCT_PATTERN;
            $cat_pfx          = 'shop';
            $category_pattern = self::DEFAULT_CATEGORY_PATTERN;
        }

        self::$patterns = [
            'product_prefix'   => $prod_pfx,
            'category_prefix'  => $cat_pfx,
            'product_pattern'  => $product_pattern,
            'category_pattern' => $category_pattern,
        ];

        return self::$patterns;
    }

    /**
     * Extract the first path segment from a URL pattern.
     * All whitelisted patterns have a static literal as the first segment.
     */
    private static function extract_prefix( string $pattern ): string {
        $segments = array_filter( explode( '/', trim( $pattern, '/' ) ) );
        $first    = reset( $segments );
        return $first ?: '';
    }

    /**
     * Reset cached patterns (call after saving SEO settings).
     */
    public static function reset_cache(): void {
        self::$patterns = null;
    }

    /* ──────────────────────────────────────────────────────────────────────────
     * WordPress rewrite rules — DYNAMIC based on saved patterns
     * ────────────────────────────────────────────────────────────────────────── */

    /**
     * Register all TitanCart rewrite rules.
     * Hooked to 'init'.
     *
     * Prefixes are always non-empty validated strings (never root-level),
     * so rewrite rules can never conflict with WordPress pages.
     */
    public static function register_rewrite_rules(): void {
        $p        = self::get_url_patterns();
        $prod_pfx = preg_quote( $p['product_prefix'], '/' );
        $cat_pfx  = preg_quote( $p['category_prefix'], '/' );

        // ── Product: e.g. ^product/([^/]+)/?$ ──
        add_rewrite_rule(
            '^' . $prod_pfx . '/([^/]+)/?$',
            'index.php?tc_product=$matches[1]',
            'top'
        );

        // ── Nested category: e.g. ^shop/{parent}/{child}/ ──
        add_rewrite_rule(
            '^' . $cat_pfx . '/([^/]+)/([^/]+)/?$',
            'index.php?tc_category=$matches[2]&tc_parent_category=$matches[1]',
            'top'
        );

        // ── Single category: e.g. ^shop/{slug}/ ──
        add_rewrite_rule(
            '^' . $cat_pfx . '/([^/]+)/?$',
            'index.php?tc_category=$matches[1]',
            'top'
        );

        // ── Shop index: e.g. ^shop/ ──
        add_rewrite_rule(
            '^' . $cat_pfx . '/?$',
            'index.php?tc_shop=1',
            'top'
        );

        // ── Tag archive: ^product-tag/{slug}/ ──
        // Hardcoded prefix avoids WP's built-in /tag/ collision for post_tag taxonomy.
        $tag_pfx = preg_quote( self::TAG_PREFIX, '/' );
        add_rewrite_rule(
            '^' . $tag_pfx . '/([^/]+)/?$',
            'index.php?tc_tag=$matches[1]',
            'top'
        );
    }

    /**
     * Register custom query vars.
     * Hooked to 'query_vars'.
     *
     * @param  array $vars
     * @return array
     */
    public static function register_query_vars( array $vars ): array {
        $vars[] = 'tc_product';
        $vars[] = 'tc_category';
        $vars[] = 'tc_parent_category';
        $vars[] = 'tc_shop';
        $vars[] = 'tc_tag';
        return $vars;
    }

    /**
     * Add canonical link tag to <head> for TitanCart pages.
     * Hooked to 'wp_head'.
     */
    public static function output_canonical(): void {
        if ( ! self::is_tc_page() ) {
            return;
        }

        $url = self::get_current_canonical();
        if ( $url ) {
            echo '<link rel="canonical" href="' . esc_url( $url ) . '" />' . "\n";
        }
    }

    /**
     * Output description, Open Graph, and Twitter Card meta tags to <head>
     * for TitanCart product and category pages.
     *
     * Reads meta_title / meta_desc / short_desc / description from the
     * translation row, falls back through them when fields are empty.
     * Defers silently if a third-party SEO plugin (Yoast, RankMath, SEOPress)
     * is active to avoid duplicate / conflicting tags.
     *
     * Hooked to 'wp_head'.
     */
    public static function output_meta_tags(): void {
        if ( ! self::is_tc_page() ) {
            return;
        }

        // Defer to dedicated SEO plugins if any are active.
        if ( defined( 'WPSEO_VERSION' )
            || defined( 'RANK_MATH_VERSION' )
            || defined( 'SEOPRESS_VERSION' )
            || defined( 'AIOSEO_VERSION' ) ) {
            return;
        }

        global $wpdb;
        $p = $wpdb->prefix . TITANCART_TABLE_PREFIX;

        $title        = '';
        $description  = '';
        $og_image     = '';
        $og_image_alt = '';
        $og_type      = 'website';
        $context      = 'shop';

        if ( $slug = get_query_var( 'tc_product' ) ) {
            $context = 'product';
            $row = $wpdb->get_row( $wpdb->prepare(
                "SELECT p.id, t.name, t.short_desc, t.description, t.meta_title, t.meta_desc
                 FROM {$p}products p
                 LEFT JOIN {$p}product_translations t ON t.product_id = p.id AND t.language_code = 'en'
                 WHERE p.slug = %s AND p.deleted_at IS NULL
                 LIMIT 1",
                sanitize_text_field( $slug )
            ) );
            if ( ! $row ) {
                return;
            }
            $title       = $row->meta_title ?: ( $row->name ?: '' );
            $description = $row->meta_desc
                ?: ( $row->short_desc
                    ?: self::trim_for_meta( wp_strip_all_tags( (string) $row->description ) ) );
            $og_type     = 'product';

            // Load main image + its saved alt_text in one query.
            $img_row = $wpdb->get_row( $wpdb->prepare(
                "SELECT wp_attachment_id, alt_text FROM {$p}product_images
                 WHERE product_id = %d
                 ORDER BY is_main DESC, position ASC
                 LIMIT 1",
                (int) $row->id
            ) );
            if ( $img_row && (int) $img_row->wp_attachment_id > 0 ) {
                $og_image     = wp_get_attachment_image_url( (int) $img_row->wp_attachment_id, 'large' ) ?: '';
                $og_image_alt = (string) ( $img_row->alt_text ?? '' );
            }
        } elseif ( $slug = get_query_var( 'tc_category' ) ) {
            $context = 'category';
            $row = $wpdb->get_row( $wpdb->prepare(
                "SELECT c.id, t.name, t.description, t.meta_title, t.meta_desc
                 FROM {$p}categories c
                 LEFT JOIN {$p}category_translations t ON t.category_id = c.id AND t.language_code = 'en'
                 WHERE c.slug = %s AND c.deleted_at IS NULL
                 LIMIT 1",
                sanitize_text_field( $slug )
            ) );
            if ( ! $row ) {
                return;
            }
            $title       = $row->meta_title ?: ( $row->name ?: '' );
            $description = $row->meta_desc
                ?: self::trim_for_meta( wp_strip_all_tags( (string) $row->description ) );
        } elseif ( get_query_var( 'tc_shop' ) ) {
            $context     = 'shop';
            $title       = __( 'Shop', 'titancart-lite' );
            $description = (string) get_bloginfo( 'description' );
        } elseif ( $tag_slug = get_query_var( 'tc_tag' ) ) {
            $context = 'tag';
            $tag_row = $wpdb->get_row( $wpdb->prepare(
                "SELECT id, name FROM {$p}tags WHERE slug = %s LIMIT 1",
                sanitize_text_field( $tag_slug )
            ) );
            if ( ! $tag_row ) {
                return;
            }
            /* translators: %s: product tag name. */
            $title       = sprintf( __( 'Products tagged "%s"', 'titancart-lite' ), $tag_row->name );
            $description = sprintf(
                /* translators: %s = tag name */
                __( 'Browse products tagged "%s".', 'titancart-lite' ),
                $tag_row->name
            );
        }

        $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '/' ) );
        $url         = self::get_current_canonical() ?: esc_url_raw( home_url( $request_uri ) );

        // Allow themes / extensions to override the full payload.
        $tags = apply_filters( 'titancart_seo_meta_tags', [
            'title'        => $title,
            'description'  => $description,
            'og_image'     => $og_image,
            'og_image_alt' => $og_image_alt,
            'og_type'      => $og_type,
            'url'          => $url,
        ], $context );

        // ── Description ────────────────────────────────────────────
        if ( ! empty( $tags['description'] ) ) {
            echo '<meta name="description" content="' . esc_attr( $tags['description'] ) . '" />' . "\n";
        }

        // ── Open Graph ─────────────────────────────────────────────
        if ( ! empty( $tags['title'] ) ) {
            echo '<meta property="og:title" content="' . esc_attr( $tags['title'] ) . '" />' . "\n";
        }
        if ( ! empty( $tags['description'] ) ) {
            echo '<meta property="og:description" content="' . esc_attr( $tags['description'] ) . '" />' . "\n";
        }
        echo '<meta property="og:type" content="' . esc_attr( $tags['og_type'] ) . '" />' . "\n";
        if ( ! empty( $tags['url'] ) ) {
            echo '<meta property="og:url" content="' . esc_url( $tags['url'] ) . '" />' . "\n";
        }
        echo '<meta property="og:site_name" content="' . esc_attr( get_bloginfo( 'name' ) ) . '" />' . "\n";
        if ( ! empty( $tags['og_image'] ) ) {
            echo '<meta property="og:image" content="' . esc_url( $tags['og_image'] ) . '" />' . "\n";
            if ( ! empty( $tags['og_image_alt'] ) ) {
                echo '<meta property="og:image:alt" content="' . esc_attr( $tags['og_image_alt'] ) . '" />' . "\n";
            }
        }

        // ── Twitter Card ───────────────────────────────────────────
        echo '<meta name="twitter:card" content="' . ( ! empty( $tags['og_image'] ) ? 'summary_large_image' : 'summary' ) . '" />' . "\n";
        if ( ! empty( $tags['title'] ) ) {
            echo '<meta name="twitter:title" content="' . esc_attr( $tags['title'] ) . '" />' . "\n";
        }
        if ( ! empty( $tags['description'] ) ) {
            echo '<meta name="twitter:description" content="' . esc_attr( $tags['description'] ) . '" />' . "\n";
        }
        if ( ! empty( $tags['og_image'] ) ) {
            echo '<meta name="twitter:image" content="' . esc_url( $tags['og_image'] ) . '" />' . "\n";
            if ( ! empty( $tags['og_image_alt'] ) ) {
                echo '<meta name="twitter:image:alt" content="' . esc_attr( $tags['og_image_alt'] ) . '" />' . "\n";
            }
        }
    }

    /**
     * Trim a plaintext string to a description-friendly length (160 chars max),
     * collapsing whitespace and appending an ellipsis when truncated.
     */
    private static function trim_for_meta( string $text, int $max = 160 ): string {
        $text = trim( preg_replace( '/\s+/', ' ', $text ) );
        if ( $text === '' ) {
            return '';
        }
        if ( function_exists( 'mb_strlen' ) && mb_strlen( $text ) > $max ) {
            return rtrim( mb_substr( $text, 0, $max - 1 ), " \t\n\r\0\x0B.,;:!?-" ) . '…';
        }
        if ( strlen( $text ) > $max ) {
            return rtrim( substr( $text, 0, $max - 1 ), " \t\n\r\0\x0B.,;:!?-" ) . '…';
        }
        return $text;
    }

    /* ──────────────────────────────────────────────────────────────────────────
     * Slug generation & uniqueness
     * ────────────────────────────────────────────────────────────────────────── */

    /**
     * Generate a URL-safe slug from a string.
     *
     * @param  string $name
     * @return string
     */
    public static function slugify( string $name ): string {
        $slug = sanitize_title( $name );
        $slug = preg_replace( '/[^a-z0-9\-]/', '', $slug );
        $slug = trim( $slug, '-' );
        return $slug ?: 'item';
    }

    /* ──────────────────────────────────────────────────────────────────────────
     * Slug resolution
     *
     * Slugs live on the authoritative base-table columns (tc_products.slug /
     * tc_categories.slug) and are managed by ProductRepository / CategoryRepository,
     * which own de-duplication (unique_slug) and slug-change redirects. These
     * helpers only READ those columns; there is no separate slug store. Both
     * scope to live rows (deleted_at IS NULL), matching every storefront read
     * path, so a soft-deleted object's slug never resolves.
     * ────────────────────────────────────────────────────────────────────────── */

    /**
     * Resolve the short table name for an SEO object type.
     */
    private static function object_table( string $object_type ): string {
        return $object_type === 'category' ? 'categories' : 'products';
    }

    /**
     * Look up a live object id by slug.
     *
     * @param  string $slug
     * @param  string $object_type  'product' | 'category'
     * @param  string $lang_code    Accepted for signature compatibility; slugs
     *                              are not per-language in this schema.
     * @return int|null
     */
    public static function get_object_id_by_slug(
        string $slug,
        string $object_type,
        string $lang_code = 'en'
    ): ?int {
        if ( $slug === '' ) {
            return null;
        }
        $db = titancart_db();
        $id = $db->getField(
            "SELECT id FROM {$db->table( self::object_table( $object_type ) )}
             WHERE slug = ?s AND deleted_at IS NULL LIMIT 1",
            $slug
        );
        return $id ? (int) $id : null;
    }

    /**
     * Get the slug for a live object, read from the authoritative base-table
     * column. Soft-deleted rows resolve to null.
     *
     * @param  string $object_type  'product' | 'category'
     * @param  int    $object_id
     * @param  string $lang_code    Accepted for signature compatibility; slugs
     *                              are not per-language in this schema.
     * @return string|null
     */
    public static function get_slug(
        string $object_type,
        int $object_id,
        string $lang_code = 'en'
    ): ?string {
        if ( $object_id <= 0 ) {
            return null;
        }
        $db   = titancart_db();
        $slug = $db->getField(
            "SELECT slug FROM {$db->table( self::object_table( $object_type ) )}
             WHERE id = ?i AND deleted_at IS NULL LIMIT 1",
            $object_id
        );
        return ( is_string( $slug ) && $slug !== '' ) ? $slug : null;
    }

    /* ──────────────────────────────────────────────────────────────────────────
     * URL builders
     * ────────────────────────────────────────────────────────────────────────── */

    /**
     * Get the frontend URL for a product.
     *
     * @param  int    $product_id
     * @param  string $lang_code
     * @return string
     */
    public static function get_product_url( int $product_id, string $lang_code = 'en' ): string {
        $slug = self::get_slug( 'product', $product_id, $lang_code ) ?? $product_id;
        $pfx  = self::get_url_patterns()['product_prefix'];
        return home_url( "/{$pfx}/{$slug}/" );
    }

    /**
     * Get the frontend URL for a category.
     */
    public static function get_category_url( int $category_id, string $lang_code = 'en', ?string $parent_slug = null ): string {
        $slug = self::get_slug( 'category', $category_id, $lang_code ) ?? $category_id;
        $pfx  = self::get_url_patterns()['category_prefix'];
        $base = $parent_slug ? "/{$pfx}/{$parent_slug}/{$slug}/" : "/{$pfx}/{$slug}/";
        return home_url( $base );
    }

    /**
     * Shop index URL.
     */
    public static function get_shop_url(): string {
        return home_url( '/' . self::get_url_patterns()['category_prefix'] . '/' );
    }

    /**
     * Get the URL base path for products. E.g. '/product'.
     */
    public static function product_base(): string {
        return '/' . self::get_url_patterns()['product_prefix'];
    }

    /**
     * Get the URL base path for categories. E.g. '/shop'.
     */
    public static function category_base(): string {
        return '/' . self::get_url_patterns()['category_prefix'];
    }

    /**
     * Build a full product URL from a slug string.
     */
    public static function product_url( string $slug ): string {
        return home_url( self::product_base() . '/' . $slug . '/' );
    }

    /**
     * Build a full category URL from a slug string.
     */
    public static function category_url( string $slug ): string {
        return home_url( self::category_base() . '/' . $slug . '/' );
    }

    /**
     * Get the URL base path for tags. E.g. '/product-tag'.
     */
    public static function tag_base(): string {
        return '/' . self::TAG_PREFIX;
    }

    /**
     * Build a full tag archive URL from a slug string.
     */
    public static function tag_url( string $slug ): string {
        return home_url( self::tag_base() . '/' . $slug . '/' );
    }

    /* ──────────────────────────────────────────────────────────────────────────
     * Helpers
     * ────────────────────────────────────────────────────────────────────────── */

    /**
     * True when the current request is a TitanCart-managed page.
     */
    public static function is_tc_page(): bool {
        return (bool) (
            get_query_var( 'tc_product' )
            || get_query_var( 'tc_category' )
            || get_query_var( 'tc_shop' )
            || get_query_var( 'tc_tag' )
        );
    }

    /**
     * Return the canonical URL for the current TitanCart page.
     */
    private static function get_current_canonical(): ?string {
        $p        = self::get_url_patterns();
        $prod_pfx = $p['product_prefix'];
        $cat_pfx  = $p['category_prefix'];

        if ( $slug = get_query_var( 'tc_product' ) ) {
            return home_url( "/{$prod_pfx}/{$slug}/" );
        }
        if ( $category = get_query_var( 'tc_category' ) ) {
            $parent = get_query_var( 'tc_parent_category' );
            return $parent
                ? home_url( "/{$cat_pfx}/{$parent}/{$category}/" )
                : home_url( "/{$cat_pfx}/{$category}/" );
        }
        if ( get_query_var( 'tc_shop' ) ) {
            return home_url( "/{$cat_pfx}/" );
        }
        if ( $tag = get_query_var( 'tc_tag' ) ) {
            return home_url( self::tag_base() . '/' . $tag . '/' );
        }
        return null;
    }

    /**
     * Flush WordPress rewrite rules.
     * Called on plugin activation / settings change.
     */
    public static function flush_rewrite_rules(): void {
        self::register_rewrite_rules();
        flush_rewrite_rules();
    }
}
