<?php

declare(strict_types=1);

/**
 * Image Attribute Pattern Engine (Task 9).
 *
 * Non-AI, deterministic generator for attachment alt/title/caption. Takes
 * a user-supplied template like `{filename} {site_name}` and resolves it
 * against the attachment's WP metadata. Designed to be:
 *
 *   - Safe — sanitizes output, never emits HTML, caps alt length, never
 *     emits an empty alt (falls back to humanized filename).
 *   - Predictable — same input always produces the same output. No AI,
 *     no external network calls, no clock-dependent fuzzing other than
 *     the `{year}` token which resolves to wp_date('Y').
 *   - Read-only at this layer — generating a value does NOT write to the
 *     database. Writing is done by `ImageAttributeHistory::apply_run()`.
 *
 * @package ProRank\SEO\Core\Media
 * @since   1.3.1
 */

namespace ProRank\SEO\Core\Media;

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


class ImageAttributePatternEngine {

    /**
     * Alt text upper bound. Spec asks for ~125 chars (the practical limit
     * screen readers stop reading at). We trim on word boundary to avoid
     * mid-word ellipses where possible.
     */
    public const ALT_SOFT_CAP = 125;

    /**
     * Title / caption are looser but still bounded so a 10kB filename
     * cannot blow up the meta row.
     */
    public const TITLE_CAP   = 160;
    public const CAPTION_CAP = 250;

    /** Recognized template tokens. */
    public const TOKENS = [
        '{filename}',
        '{attachment_title}',
        '{parent_title}',
        '{site_name}',
        '{mime_type}',
        '{year}',
    ];

    /**
     * Generate the three candidate fields for a given attachment.
     *
     * @param int   $attachment_id
     * @param array $templates {
     *   @type string $alt      Template for alt.       Optional — empty disables alt.
     *   @type string $title    Template for title.     Optional — empty disables title.
     *   @type string $caption  Template for caption.   Optional — empty disables caption.
     * }
     * @return array{alt:string,title:string,caption:string,filename_humanized:string,mime:string}
     */
    public static function generate( int $attachment_id, array $templates ): array {
        $context = self::build_context( $attachment_id );

        $alt_tpl     = isset( $templates['alt'] )     ? (string) $templates['alt']     : '';
        $title_tpl   = isset( $templates['title'] )   ? (string) $templates['title']   : '';
        $caption_tpl = isset( $templates['caption'] ) ? (string) $templates['caption'] : '';

        $alt = $alt_tpl !== ''
            ? self::sanitize_field( self::resolve_template( $alt_tpl, $context ), self::ALT_SOFT_CAP )
            : '';

        // Never emit empty alt when an alt template was supplied — fall
        // back to the humanized filename, which is always present.
        if ( $alt_tpl !== '' && $alt === '' && $context['filename_humanized'] !== '' ) {
            $alt = self::sanitize_field( $context['filename_humanized'], self::ALT_SOFT_CAP );
        }

        $title = $title_tpl !== ''
            ? self::sanitize_field( self::resolve_template( $title_tpl, $context ), self::TITLE_CAP )
            : '';

        $caption = $caption_tpl !== ''
            ? self::sanitize_field( self::resolve_template( $caption_tpl, $context ), self::CAPTION_CAP )
            : '';

        return [
            'alt'                 => $alt,
            'title'               => $title,
            'caption'             => $caption,
            'filename_humanized'  => $context['filename_humanized'],
            'mime'                => $context['mime_type'],
        ];
    }

    /**
     * Public helper for the UI's pattern preview (no attachment needed).
     */
    public static function resolve( string $template, array $context_overrides = [] ): string {
        $context = array_merge( self::default_context(), $context_overrides );
        return self::resolve_template( $template, $context );
    }

    /**
     * Recognize and return only the tokens used in a template — handy for
     * the UI to preview which fields will pull from where.
     *
     * @return array<int,string>
     */
    public static function tokens_in( string $template ): array {
        $found = [];
        foreach ( self::TOKENS as $token ) {
            if ( strpos( $template, $token ) !== false ) {
                $found[] = $token;
            }
        }
        return $found;
    }

    // ----------------------------------------------------------------
    // Internals
    // ----------------------------------------------------------------

    /**
     * Build the full token context for one attachment. Read-only; never
     * writes to the database.
     */
    private static function build_context( int $attachment_id ): array {
        $context = self::default_context();
        if ( $attachment_id <= 0 ) {
            return $context;
        }

        $post = function_exists( 'get_post' ) ? get_post( $attachment_id ) : null;
        if ( ! $post ) {
            return $context;
        }

        $mime  = (string) ( $post->post_mime_type ?? '' );
        $title = (string) ( $post->post_title ?? '' );

        $file_path = function_exists( 'get_attached_file' ) ? (string) get_attached_file( $attachment_id ) : '';
        $basename  = $file_path !== '' ? basename( $file_path ) : '';
        if ( $basename === '' ) {
            // Fallback: try the URL.
            if ( function_exists( 'wp_get_attachment_url' ) ) {
                $url = (string) wp_get_attachment_url( $attachment_id );
                if ( $url !== '' ) {
                    $basename = basename( wp_parse_url( $url, PHP_URL_PATH ) ?: '' );
                }
            }
        }
        $filename_humanized = self::humanize_filename( $basename );

        $parent_title = '';
        if ( ! empty( $post->post_parent ) ) {
            $parent = function_exists( 'get_post' ) ? get_post( (int) $post->post_parent ) : null;
            if ( $parent ) {
                $parent_title = (string) ( $parent->post_title ?? '' );
            }
        }

        $context['filename']           = $basename;
        $context['filename_humanized'] = $filename_humanized;
        $context['attachment_title']   = $title;
        $context['parent_title']       = $parent_title;
        $context['mime_type']          = $mime;
        return $context;
    }

    private static function default_context(): array {
        $site_name = '';
        if ( function_exists( 'get_bloginfo' ) ) {
            $site_name = (string) get_bloginfo( 'name' );
        }
        $year = '';
        if ( function_exists( 'wp_date' ) ) {
            $year = (string) wp_date( 'Y' );
        } elseif ( function_exists( 'date_i18n' ) ) {
            $year = (string) date_i18n( 'Y' );
        } else {
            $year = (string) gmdate( 'Y' );
        }
        return [
            'filename'           => '',
            'filename_humanized' => '',
            'attachment_title'   => '',
            'parent_title'       => '',
            'site_name'          => $site_name,
            'mime_type'          => '',
            'year'               => $year,
        ];
    }

    /**
     * Token substitution. We resolve `filename_humanized` for `{filename}`
     * (users almost always want the readable form, not "red-light-panel.jpg").
     * The raw basename is intentionally not exposed as a separate token —
     * a URL-shaped string makes terrible alt text.
     */
    private static function resolve_template( string $template, array $context ): string {
        $map = [
            '{filename}'         => (string) ( $context['filename_humanized'] ?? '' ),
            '{attachment_title}' => (string) ( $context['attachment_title']   ?? '' ),
            '{parent_title}'     => (string) ( $context['parent_title']       ?? '' ),
            '{site_name}'        => (string) ( $context['site_name']          ?? '' ),
            '{mime_type}'        => (string) ( $context['mime_type']          ?? '' ),
            '{year}'             => (string) ( $context['year']               ?? '' ),
        ];
        $out = strtr( $template, $map );
        // Collapse the holes a missing token leaves behind ("  " → " ").
        $out = preg_replace( '/\s+/u', ' ', (string) $out ) ?? $out;
        return trim( (string) $out );
    }

    /**
     * Filename → human-readable string.
     *
     * - Drops the extension and any size suffix Photoshop / WP adds (e.g.
     *   `-150x150`, `-scaled`, `-1`).
     * - Replaces `-` and `_` with space.
     * - Strips obvious noise (camera/CMS hash prefixes longer than 12 chars
     *   are left intact — we cannot reliably distinguish them from words).
     * - Collapses whitespace and trims.
     */
    public static function humanize_filename( string $basename ): string {
        if ( $basename === '' ) {
            return '';
        }
        // Remove path artifacts, just in case.
        $basename = basename( $basename );
        // Drop extension.
        $dot = strrpos( $basename, '.' );
        if ( $dot !== false && $dot > 0 ) {
            $basename = substr( $basename, 0, $dot );
        }
        // Drop WP size suffix like -150x150 / -scaled / -rotated.
        $basename = preg_replace( '/-\d+x\d+$/u', '', $basename ) ?? $basename;
        $basename = preg_replace( '/-(scaled|rotated|edited|cropped)$/iu', '', $basename ) ?? $basename;
        // Replace separators with space.
        $basename = strtr( (string) $basename, [ '-' => ' ', '_' => ' ', '.' => ' ' ] );
        // Collapse whitespace + remove control chars.
        $basename = preg_replace( '/[[:cntrl:]]+/u', ' ', $basename ) ?? $basename;
        $basename = preg_replace( '/\s+/u', ' ', (string) $basename ) ?? '';
        return trim( (string) $basename );
    }

    /**
     * Final scrub before a value can be written to the DB or rendered
     * back to the UI. Strips HTML + control chars, normalizes whitespace,
     * and trims to the field-specific cap. For caps over 60 chars we try
     * to trim on a word boundary so the ellipsis lands cleanly.
     */
    public static function sanitize_field( string $value, int $cap ): string {
        if ( $value === '' ) {
            return '';
        }
        // Strip tags + control chars first.
        $value = function_exists( 'wp_strip_all_tags' )
            ? (string) wp_strip_all_tags( $value )
            : strip_tags( $value );
        $value = preg_replace( '/[[:cntrl:]]+/u', ' ', $value ) ?? $value;
        $value = preg_replace( '/\s+/u', ' ', (string) $value ) ?? '';
        $value = trim( (string) $value );

        if ( $value === '' ) {
            return '';
        }

        $len = function_exists( 'mb_strlen' ) ? mb_strlen( $value, 'UTF-8' ) : strlen( $value );
        if ( $len <= $cap ) {
            return $value;
        }
        $slice = function_exists( 'mb_substr' ) ? mb_substr( $value, 0, $cap, 'UTF-8' ) : substr( $value, 0, $cap );
        if ( $cap > 60 ) {
            // Trim to last whitespace so we don't break mid-word.
            $last_space = function_exists( 'mb_strrpos' ) ? mb_strrpos( $slice, ' ', 0, 'UTF-8' ) : strrpos( $slice, ' ' );
            if ( $last_space !== false && $last_space > intdiv( $cap, 2 ) ) {
                $slice = function_exists( 'mb_substr' ) ? mb_substr( $slice, 0, $last_space, 'UTF-8' ) : substr( $slice, 0, $last_space );
            }
        }
        return rtrim( (string) $slice, " \t\n\r\0\x0B,.;:-_" ) . '…';
    }
}
