<?php
/**
 * Chunk source attribution helpers.
 *
 * sitestaffr_sanitize_source_chunks(): validate/cap an attribution array for storage.
 * sitestaffr_render_source_chunks(): render a collapsed <details> disclosure for display.
 *
 * @package SiteStaffr
 */

if ( ! defined( 'ABSPATH' ) ) {
    // Allow CLI self-test without full WP bootstrap.
    if ( ! function_exists( 'esc_html' ) ) {
        function esc_html( $s ) { return htmlspecialchars( (string) $s, ENT_QUOTES ); }
    }
    if ( ! function_exists( 'esc_url' ) ) {
        function esc_url( $s ) { return filter_var( (string) $s, FILTER_SANITIZE_URL ); }
    }
}

if ( ! defined( 'SITESTAFFR_SOURCE_CHUNKS_MAX' ) ) {
    define( 'SITESTAFFR_SOURCE_CHUNKS_MAX', 15 );
}
if ( ! defined( 'SITESTAFFR_SOURCE_CHUNK_CONTENT_MAX' ) ) {
    define( 'SITESTAFFR_SOURCE_CHUNK_CONTENT_MAX', 8000 );
}
// Display-only "match" band thresholds (raw cosine on ~400-token chunks runs small;
// a banded label is friendlier than a scary 0.30). Tunable.
if ( ! defined( 'SITESTAFFR_SOURCE_MATCH_HIGH' ) ) {
    define( 'SITESTAFFR_SOURCE_MATCH_HIGH', 0.35 );
}
if ( ! defined( 'SITESTAFFR_SOURCE_MATCH_MED' ) ) {
    define( 'SITESTAFFR_SOURCE_MATCH_MED', 0.25 );
}

/**
 * Validate and cap an attribution array; return a JSON string for storage, or null.
 *
 * @param mixed $raw Decoded array of chunk objects (from the saved transcript turn).
 * @return string|null JSON string, or null when there is nothing valid to store.
 */
function sitestaffr_sanitize_source_chunks( $raw ) {
    if ( ! is_array( $raw ) || empty( $raw ) ) {
        return null;
    }
    $out = array();
    foreach ( $raw as $chunk ) {
        if ( ! is_array( $chunk ) ) {
            continue;
        }
        $content = isset( $chunk['content'] ) ? (string) $chunk['content'] : '';
        if ( strlen( $content ) > SITESTAFFR_SOURCE_CHUNK_CONTENT_MAX ) {
            $content = substr( $content, 0, SITESTAFFR_SOURCE_CHUNK_CONTENT_MAX );
        }
        $score = null;
        if ( isset( $chunk['relevance_score'] ) && is_numeric( $chunk['relevance_score'] ) ) {
            $score = round( (float) $chunk['relevance_score'], 4 );
        }
        $out[] = array(
            'post_id'         => isset( $chunk['post_id'] ) ? $chunk['post_id'] : null,
            'chunk_index'     => isset( $chunk['chunk_index'] ) && is_numeric( $chunk['chunk_index'] ) ? (int) $chunk['chunk_index'] : null,
            'post_title'      => isset( $chunk['post_title'] ) ? (string) $chunk['post_title'] : null,
            'relevance_score' => $score,
            'query'           => isset( $chunk['query'] ) ? (string) $chunk['query'] : null,
            'content'         => $content,
        );
        if ( count( $out ) >= SITESTAFFR_SOURCE_CHUNKS_MAX ) {
            break;
        }
    }
    if ( empty( $out ) ) {
        return null;
    }
    return sitestaffr_source_chunks_json_encode( $out );
}

/**
 * JSON encode that works under CLI self-test (no WP) and inside WP.
 *
 * @param mixed $data
 * @return string
 */
function sitestaffr_source_chunks_json_encode( $data ) {
    if ( function_exists( 'wp_json_encode' ) ) {
        return wp_json_encode( $data );
    }
    return json_encode( $data );
}

/**
 * Map a raw relevance score to a friendly "match" band for display.
 *
 * @param float|null $score
 * @return string|null 'high' | 'medium' | 'low', or null when no score.
 */
function sitestaffr_source_match_band( $score ) {
    if ( $score === null || ! is_numeric( $score ) ) {
        return null;
    }
    $score = (float) $score;
    if ( $score >= SITESTAFFR_SOURCE_MATCH_HIGH ) {
        return 'high';
    }
    if ( $score >= SITESTAFFR_SOURCE_MATCH_MED ) {
        return 'medium';
    }
    return 'low';
}

/**
 * Build a "scroll-to-text-fragment" deep link to the exact spot on the source
 * page (https://example.com/page/#:~:text=<snippet>). Modern browsers scroll to
 * and highlight the matched text; older ones land at the top of the page.
 *
 * Returns '' when no distinctive link can be built (caller falls back).
 *
 * @param string $permalink Source page URL.
 * @param string $content   Chunk content to derive the snippet from.
 * @return string Deep link URL, or '' to decline.
 */
function sitestaffr_source_text_fragment_url( $permalink, $content ) {
    $permalink = (string) $permalink;
    if ( $permalink === '' ) {
        return '';
    }
    // Collapse whitespace and drop a leading markdown-ish heading marker.
    $text = trim( preg_replace( '/\s+/', ' ', (string) $content ) );
    $text = preg_replace( '/^#+\s*/', '', $text );
    if ( $text === '' ) {
        return '';
    }
    // Take the first ~8 words / ~60 chars on a word boundary as a distinctive anchor.
    $snippet = $text;
    if ( strlen( $snippet ) > 60 ) {
        $snippet = substr( $snippet, 0, 60 );
        $last_space = strrpos( $snippet, ' ' );
        if ( $last_space !== false && $last_space >= 12 ) {
            $snippet = substr( $snippet, 0, $last_space );
        }
    }
    $words = preg_split( '/\s+/', $snippet );
    if ( is_array( $words ) && count( $words ) > 8 ) {
        $snippet = implode( ' ', array_slice( $words, 0, 8 ) );
    }
    $snippet = trim( $snippet );
    if ( strlen( $snippet ) < 12 ) {
        return '';
    }
    // Text Fragments percent-encode the directive value; '-' is reserved as the
    // range separator, so encode it too.
    $encoded = str_replace( '-', '%2D', rawurlencode( $snippet ) );
    return $permalink . '#:~:text=' . $encoded;
}

/**
 * Render a collapsed <details> "Source" disclosure for an AI message.
 *
 * Shows only the single highest-relevance retrieved chunk (the most likely
 * source of the answer). See the design doc / wiki for why we don't show all
 * retrieved chunks: the middleware feeds every retrieved chunk to the model and
 * does not track which one(s) it actually used.
 *
 * @param string|null $json The stored source_chunks JSON, or null.
 * @return string HTML (empty string when there is nothing to show).
 */
function sitestaffr_render_source_chunks( $json ) {
    if ( empty( $json ) ) {
        return '';
    }
    $chunks = json_decode( $json, true );
    if ( ! is_array( $chunks ) || empty( $chunks ) ) {
        return '';
    }

    // Pick the top chunk by relevance. The stored array is already sorted
    // descending, but choose defensively in case an older row is unsorted.
    $top = $chunks[0];
    foreach ( $chunks as $chunk ) {
        if ( ! is_array( $chunk ) ) {
            continue;
        }
        $best = isset( $top['relevance_score'] ) ? (float) $top['relevance_score'] : -1.0;
        $cur  = isset( $chunk['relevance_score'] ) ? (float) $chunk['relevance_score'] : -1.0;
        if ( $cur > $best ) {
            $top = $chunk;
        }
    }
    if ( ! is_array( $top ) ) {
        return '';
    }

    $title = isset( $top['post_title'] ) ? esc_html( $top['post_title'] ) : '(untitled)';
    $band  = isset( $top['relevance_score'] ) ? sitestaffr_source_match_band( $top['relevance_score'] ) : null;
    $match = $band ? ' &middot; match: ' . esc_html( $band ) : '';
    $query = ! empty( $top['query'] )
        ? ' &middot; searched for &ldquo;' . esc_html( $top['query'] ) . '&rdquo;'
        : '';
    $content = isset( $top['content'] ) ? (string) $top['content'] : '';

    $html  = '<details class="sitestaffr-tv-sources">';
    $html .= '<summary class="sitestaffr-tv-sources-summary">Source</summary>';
    $html .= '<div class="sitestaffr-tv-sources-body">';
    $html .= '<div class="sitestaffr-tv-source">';
    $html .= '<div class="sitestaffr-tv-source-meta"><strong>' . $title . '</strong>' . $match . $query . '</div>';
    $html .= '<div class="sitestaffr-tv-source-content">' . nl2br( esc_html( $content ) ) . '</div>';

    if ( ! empty( $top['post_id'] ) && function_exists( 'get_permalink' ) ) {
        $url = get_permalink( (int) $top['post_id'] );
        if ( $url ) {
            $deep_link = sitestaffr_source_text_fragment_url( $url, $content );
            if ( $deep_link !== '' ) {
                $html .= '<a class="sitestaffr-tv-source-link" href="' . esc_url( $deep_link ) . '" target="_blank" rel="noopener noreferrer">Jump to this section</a>';
            } else {
                $html .= '<a class="sitestaffr-tv-source-link" href="' . esc_url( $url ) . '" target="_blank" rel="noopener noreferrer">View page</a>';
            }
        }
    }

    $html .= '</div>';
    $html .= '</div></details>';
    return $html;
}
