<?php
/**
 * Brand profile: a per-site, cached summary of the site's existing
 * design language and voice. Fed to the Co-pilot designers (Opus/Haiku)
 * so newly generated pages match the look, typography, tone, and photo
 * style of pages already on the site — instead of inventing a fresh
 * brand each time.
 *
 * Storage: WP option `sailwp_copilot_brand_profile` (auto-extracted).
 * Overrides: WP option `sailwp_copilot_brand_profile_overrides` (sparse;
 * any key set here wins over the auto-extracted value at merge time).
 *
 * Invalidated automatically when posts/pages or attachments change so
 * the profile stays current without the user thinking about it.
 */
if (!defined('ABSPATH')) exit;

class SailWP_Copilot_Brand_Profile {
    const OPTION_PROFILE   = 'sailwp_copilot_brand_profile';
    const OPTION_OVERRIDES = 'sailwp_copilot_brand_profile_overrides';

    public function init() {
        add_action('save_post',     [__CLASS__, 'invalidate_on_post_change'], 10, 2);
        add_action('post_updated',  [__CLASS__, 'invalidate_on_post_change'], 10, 1);
        add_action('deleted_post',  [__CLASS__, 'invalidate']);
        add_action('add_attachment',    [__CLASS__, 'invalidate']);
        add_action('delete_attachment', [__CLASS__, 'invalidate']);
    }

    public static function invalidate_on_post_change($post_id, $post = null) {
        $post = $post ?: get_post($post_id);
        if (!$post) return;
        if ($post->post_type !== 'page' && $post->post_type !== 'post') return;
        if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) return;
        if (get_post_meta($post_id, '_swcp_is_preview', true)) return;
        self::invalidate();
    }

    public static function invalidate() {
        delete_option(self::OPTION_PROFILE);
    }

    /**
     * Get the merged brand profile (auto-extracted + user overrides).
     * Builds and caches on first call after invalidation.
     */
    public static function get($force_rebuild = false) {
        if ($force_rebuild) self::invalidate();
        $cached = get_option(self::OPTION_PROFILE);
        if (!is_array($cached) || empty($cached['extracted_at'])) {
            $cached = self::build();
            update_option(self::OPTION_PROFILE, $cached, false);
        }
        return self::merge_overrides($cached);
    }

    public static function get_overrides() {
        $overrides = get_option(self::OPTION_OVERRIDES);
        return is_array($overrides) ? $overrides : [];
    }

    public static function save_overrides($overrides) {
        $clean = self::sanitize_overrides($overrides);
        update_option(self::OPTION_OVERRIDES, $clean, false);
        return $clean;
    }

    public static function reset_overrides() {
        delete_option(self::OPTION_OVERRIDES);
    }

    /**
     * Auto-extract a fresh profile from the site. Pulls colors+fonts from
     * theme.json (block themes) and falls back to common customizer keys.
     * Tone samples come from the most recently modified pages/posts.
     * Photo style is inferred from recent attachments' alt-text + sources.
     */
    public static function build() {
        $profile = [
            'extracted_at' => time(),
            'site' => [
                'name'    => (string) get_bloginfo('name'),
                'tagline' => (string) get_bloginfo('description'),
                'url'     => home_url(),
                'locale'  => get_locale(),
            ],
            'theme'     => self::extract_theme(),
            'colors'    => self::extract_colors(),
            'fonts'     => self::extract_fonts(),
            'tone'      => self::extract_tone_samples(),
            'photos'    => self::extract_photo_style(),
        ];
        return $profile;
    }

    private static function extract_theme() {
        $theme = wp_get_theme();
        if (!$theme) return [];
        return [
            'name'   => (string) $theme->get('Name'),
            'author' => (string) $theme->get('Author'),
        ];
    }

    private static function extract_colors() {
        $palette = [];
        if (function_exists('wp_get_global_settings')) {
            $g = wp_get_global_settings();
            $theme_palette   = $g['color']['palette']['theme']   ?? [];
            $default_palette = $g['color']['palette']['default'] ?? [];
            foreach (array_merge($theme_palette, $default_palette) as $c) {
                if (!empty($c['color']) && !empty($c['slug'])) {
                    $palette[(string) $c['slug']] = (string) $c['color'];
                }
            }
        }
        // Customizer fallbacks for non-FSE themes
        $bg = get_theme_mod('background_color');
        if ($bg && empty($palette['background'])) {
            $palette['background'] = '#' . ltrim($bg, '#');
        }
        $header = get_theme_mod('header_textcolor');
        if ($header && $header !== 'blank' && empty($palette['header_text'])) {
            $palette['header_text'] = '#' . ltrim($header, '#');
        }
        return $palette;
    }

    private static function extract_fonts() {
        $fonts = [];
        if (function_exists('wp_get_global_settings')) {
            $g = wp_get_global_settings();
            $theme_fonts   = $g['typography']['fontFamilies']['theme']   ?? [];
            $default_fonts = $g['typography']['fontFamilies']['default'] ?? [];
            foreach (array_merge($theme_fonts, $default_fonts) as $f) {
                if (!empty($f['fontFamily']) && !empty($f['slug'])) {
                    $fonts[(string) $f['slug']] = (string) $f['fontFamily'];
                }
            }
        }
        return $fonts;
    }

    /**
     * Sample existing copy so Claude can match the writing voice.
     * Pulls headings + paragraph excerpts from up to 5 recently
     * modified pages/posts (skipping previews and trash).
     */
    private static function extract_tone_samples() {
        $samples = ['headings' => [], 'paragraphs' => []];

        $posts = get_posts([
            'post_type'      => ['page', 'post'],
            'post_status'    => 'publish',
            'orderby'        => 'modified',
            'order'          => 'DESC',
            'posts_per_page' => 8,
            'meta_query'     => [
                ['key' => '_swcp_is_preview', 'compare' => 'NOT EXISTS'],
            ],
        ]);

        foreach ($posts as $p) {
            $content = (string) $p->post_content;
            // Skip Co-pilot raw-HTML designed pages — their wp:html block
            // would dump huge inline CSS into our tone samples.
            if (strpos($content, '<!-- wp:html') !== false) continue;

            // Headings from H1-H3
            if (preg_match_all('#<h[1-3][^>]*>(.+?)</h[1-3]>#is', $content, $hm)) {
                foreach ($hm[1] as $h) {
                    $t = trim(wp_strip_all_tags($h));
                    if ($t !== '' && mb_strlen($t) <= 140) $samples['headings'][] = $t;
                }
            }
            // Paragraphs (first 240 chars each, skip tiny ones)
            if (preg_match_all('#<p[^>]*>(.+?)</p>#is', $content, $pm)) {
                foreach ($pm[1] as $par) {
                    $t = trim(wp_strip_all_tags($par));
                    $len = mb_strlen($t);
                    if ($len >= 60 && $len <= 600) {
                        $samples['paragraphs'][] = mb_substr($t, 0, 240);
                    }
                }
            }

            if (count($samples['headings']) >= 8 && count($samples['paragraphs']) >= 5) break;
        }

        $samples['headings']   = array_values(array_unique(array_slice($samples['headings'], 0, 8)));
        $samples['paragraphs'] = array_values(array_unique(array_slice($samples['paragraphs'], 0, 5)));
        return $samples;
    }

    /**
     * Look at the last ~15 attachments to spot a photo pattern: are most
     * uploads from a stock domain (Unsplash/Pexels) or from this site's
     * own uploads dir? What does the alt-text usually describe?
     */
    private static function extract_photo_style() {
        $attachments = get_posts([
            'post_type'      => 'attachment',
            'post_mime_type' => 'image',
            'post_status'    => 'inherit',
            'orderby'        => 'date',
            'order'          => 'DESC',
            'posts_per_page' => 15,
        ]);
        if (empty($attachments)) return ['count' => 0];

        $alt_samples = [];
        $sources     = ['own_uploads' => 0, 'external' => 0];
        $home_host   = parse_url(home_url(), PHP_URL_HOST);

        foreach ($attachments as $a) {
            $alt = get_post_meta($a->ID, '_wp_attachment_image_alt', true);
            if ($alt) {
                $alt = trim((string) $alt);
                if ($alt !== '' && mb_strlen($alt) <= 200) $alt_samples[] = $alt;
            }
            $src = wp_get_attachment_url($a->ID);
            if ($src) {
                $host = parse_url($src, PHP_URL_HOST);
                if ($host === $home_host) $sources['own_uploads']++;
                else $sources['external']++;
            }
        }

        $alt_samples = array_values(array_unique(array_slice($alt_samples, 0, 6)));
        return [
            'count'       => count($attachments),
            'alt_samples' => $alt_samples,
            'sources'     => $sources,
        ];
    }

    /**
     * Apply user overrides on top of auto-extracted profile.
     * Overrides are sparse — only present keys win.
     */
    private static function merge_overrides($profile) {
        $overrides = self::get_overrides();
        if (empty($overrides)) return $profile;

        // Top-level scalar/array overrides
        foreach (['colors', 'fonts', 'tone_note', 'photo_note', 'voice_guidelines'] as $key) {
            if (array_key_exists($key, $overrides)) {
                if ($key === 'tone_note' || $key === 'photo_note' || $key === 'voice_guidelines') {
                    $profile[$key] = $overrides[$key];
                } else {
                    // colors/fonts: merge (override individual slugs, keep others)
                    $existing = isset($profile[$key]) && is_array($profile[$key]) ? $profile[$key] : [];
                    $profile[$key] = array_merge($existing, (array) $overrides[$key]);
                }
            }
        }
        $profile['_has_overrides'] = true;
        return $profile;
    }

    private static function sanitize_overrides($overrides) {
        if (!is_array($overrides)) return [];
        $clean = [];

        if (isset($overrides['colors']) && is_array($overrides['colors'])) {
            $colors = [];
            foreach ($overrides['colors'] as $slug => $color) {
                $slug  = sanitize_key((string) $slug);
                $color = sanitize_hex_color((string) $color);
                if ($slug && $color) $colors[$slug] = $color;
            }
            if ($colors) $clean['colors'] = $colors;
        }
        if (isset($overrides['fonts']) && is_array($overrides['fonts'])) {
            $fonts = [];
            foreach ($overrides['fonts'] as $slug => $family) {
                $slug   = sanitize_key((string) $slug);
                $family = sanitize_text_field((string) $family);
                if ($slug && $family) $fonts[$slug] = $family;
            }
            if ($fonts) $clean['fonts'] = $fonts;
        }
        foreach (['tone_note', 'photo_note', 'voice_guidelines'] as $key) {
            if (isset($overrides[$key])) {
                $val = sanitize_textarea_field((string) $overrides[$key]);
                if ($val !== '') $clean[$key] = $val;
            }
        }
        return $clean;
    }

    /**
     * Compact payload for the proxy. Drops verbose internals, keeps
     * the bits Claude actually uses to design with.
     */
    public static function for_proxy() {
        $p = self::get();
        $out = [
            'site'   => $p['site']   ?? [],
            'theme'  => $p['theme']  ?? [],
            'colors' => $p['colors'] ?? [],
            'fonts'  => $p['fonts']  ?? [],
        ];
        if (!empty($p['tone']['headings']))   $out['tone_headings']   = $p['tone']['headings'];
        if (!empty($p['tone']['paragraphs'])) $out['tone_paragraphs'] = $p['tone']['paragraphs'];
        if (!empty($p['photos']['alt_samples'])) $out['photo_alt_samples'] = $p['photos']['alt_samples'];
        if (!empty($p['photos']['sources']))     $out['photo_sources']     = $p['photos']['sources'];
        foreach (['tone_note', 'photo_note', 'voice_guidelines'] as $k) {
            if (!empty($p[$k])) $out[$k] = $p[$k];
        }
        // Tagline from onboarding step 1 — single sentence about what
        // the site is. Goes into every design + image-gen prompt as the
        // canonical "what is this site" hint. v0.24.8 Joost 2026-05-17.
        $tagline = trim((string) get_option('sailwp_copilot_brand_tagline', ''));
        if ($tagline !== '') $out['tagline'] = $tagline;
        return $out;
    }
}
