<?php
/**
 * Pure presentation helpers for the "Eigenvergleich" (self-comparison)
 * dashboard widgets. Everything here takes already-fetched data (from
 * Seoforge_History::series()/compare()/capture_counts()) and returns plain
 * arrays for a view to render — no $wpdb, no queries, so every case (delta
 * direction, "lower is better" inversion, sorting, sparkline geometry,
 * degradation state) is directly unit-testable.
 *
 * @package Brainwerk SEO Suite
 */

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

class Seoforge_History_Widgets {

    /* ------------------------------------------------------------- delta */

    /**
     * Format a current-vs-previous comparison into direction + sentiment,
     * never relying on color alone downstream (the view pairs 'direction'
     * with an icon/arrow and 'sentiment' with a status word).
     *
     * @param float|int|null $current
     * @param float|int|null $previous
     * @param bool           $lower_is_better e.g. bounce rate, fail rates.
     * @return array{
     *   has_data: bool, current: float|null, previous: float|null,
     *   delta: float|null, direction: string|null (up|down|flat),
     *   sentiment: string|null (good|bad|neutral)
     * }
     */
    public static function format_delta( $current, $previous, $lower_is_better = false ) {
        if ( ! self::is_numeric( $current ) || ! self::is_numeric( $previous ) ) {
            return array(
                'has_data'  => false,
                'current'   => self::is_numeric( $current ) ? (float) $current : null,
                'previous'  => null,
                'delta'     => null,
                'direction' => null,
                'sentiment' => null,
            );
        }

        $current  = (float) $current;
        $previous = (float) $previous;
        $delta    = $current - $previous;

        if ( 0.0 === $delta ) {
            $direction = 'flat';
            $sentiment = 'neutral';
        } else {
            $direction = $delta > 0 ? 'up' : 'down';
            $improved  = $lower_is_better ? ( $delta < 0 ) : ( $delta > 0 );
            $sentiment = $improved ? 'good' : 'bad';
        }

        return array(
            'has_data'  => true,
            'current'   => $current,
            'previous'  => $previous,
            'delta'     => $delta,
            'direction' => $direction,
            'sentiment' => $sentiment,
        );
    }

    private static function is_numeric( $v ) {
        return is_int( $v ) || is_float( $v ) || ( is_string( $v ) && is_numeric( $v ) );
    }

    /* ---------------------------------------------------- analyzer sort */

    /**
     * Sort analyzer-check rows so the biggest problems (worsening fastest)
     * and the biggest improvements surface first, on both ends of a single
     * list: rows without a comparable delta (no_history) sort last.
     *
     * @param array $rows list of ['key','label','current','previous','delta'(nullable)].
     * @return array same rows, re-ordered (delta desc, i.e. worsening first).
     */
    public static function sort_analyzer_trend( array $rows ) {
        usort( $rows, function ( $a, $b ) {
            $da = isset( $a['delta'] ) ? $a['delta'] : null;
            $db = isset( $b['delta'] ) ? $b['delta'] : null;
            if ( null === $da && null === $db ) {
                return self::cmp_current( $a, $b );
            }
            if ( null === $da ) {
                return 1; // no-delta rows sort last
            }
            if ( null === $db ) {
                return -1;
            }
            $cmp = abs( $db ) <=> abs( $da ); // biggest movers first
            if ( 0 !== $cmp ) {
                return $cmp;
            }
            return self::cmp_current( $a, $b );
        } );
        return array_values( $rows );
    }

    private static function cmp_current( array $a, array $b ) {
        $ca = isset( $a['current'] ) ? (float) $a['current'] : 0.0;
        $cb = isset( $b['current'] ) ? (float) $b['current'] : 0.0;
        return $cb <=> $ca; // higher fail rate first on ties
    }

    /* ------------------------------------------------------------- sparkline */

    /**
     * Pure geometry for a small inline-SVG sparkline: turns an ordered list
     * of values (null = gap day, e.g. plugin was inactive) into one or more
     * polyline path segments — a gap breaks the line, it is never drawn as
     * a zero.
     *
     * @param array $values ordered oldest-first, null for a missing day.
     * @param int   $width  viewBox width.
     * @param int   $height viewBox height.
     * @param float $pad    vertical padding so the line never touches the edge.
     * @return array{
     *   segments: string[]  one "M x y L x y ..." path per contiguous run,
     *   last_point: array{0:float,1:float}|null  coordinates of the last known value,
     *   has_data: bool
     * }
     */
    public static function sparkline_path( array $values, $width = 120, $height = 32, $pad = 3 ) {
        $n = count( $values );
        if ( 0 === $n ) {
            return array( 'segments' => array(), 'last_point' => null, 'has_data' => false );
        }

        $numeric = array_filter( $values, array( __CLASS__, 'is_numeric' ) );
        if ( empty( $numeric ) ) {
            return array( 'segments' => array(), 'last_point' => null, 'has_data' => false );
        }
        $min = min( $numeric );
        $max = max( $numeric );
        $range = ( $max - $min ) > 0 ? ( $max - $min ) : 1.0;

        $plot_h = $height - 2 * $pad;
        $step   = $n > 1 ? $width / ( $n - 1 ) : 0;

        $segments   = array();
        $current    = '';
        $last_point = null;

        foreach ( $values as $i => $v ) {
            if ( ! self::is_numeric( $v ) ) {
                if ( '' !== $current ) {
                    $segments[] = trim( $current );
                    $current    = '';
                }
                continue;
            }
            $x = $n > 1 ? $i * $step : $width / 2;
            $y = $pad + $plot_h * ( 1 - ( ( (float) $v - $min ) / $range ) );
            $current   .= ( '' === $current ? 'M' : 'L' ) . self::num( $x ) . ' ' . self::num( $y ) . ' ';
            $last_point = array( $x, $y );
        }
        if ( '' !== $current ) {
            $segments[] = trim( $current );
        }

        return array(
            'segments'   => $segments,
            'last_point' => $last_point,
            'has_data'   => true,
        );
    }

    private static function num( $v ) {
        return rtrim( rtrim( number_format( (float) $v, 2, '.', '' ), '0' ), '.' );
    }

    /**
     * The most recent non-null value in a series() result — used for a
     * "current shape" snapshot (e.g. the latest captured hour/weekday
     * profile) where averaging across days would blur the profile shape.
     *
     * @param array $series list of ['date','value'] as returned by Seoforge_History::series().
     * @return mixed|null
     */
    public static function last_value( array $series ) {
        for ( $i = count( $series ) - 1; $i >= 0; $i-- ) {
            if ( isset( $series[ $i ]['value'] ) && null !== $series[ $i ]['value'] ) {
                return $series[ $i ]['value'];
            }
        }
        return null;
    }

    /* --------------------------------------------------------- degradation */

    /**
     * Classify how much history is available for a current-vs-previous
     * comparison, so a view can pick the honest rendering: nothing at all,
     * a partial/still-filling window, or a full comparison.
     *
     * @param int $current_days  rows captured in the current window.
     * @param int $previous_days rows captured in the previous window.
     * @param int $window_days   the window size in days (the "full" bar).
     * @return string 'empty'|'partial'|'full'
     */
    public static function comparison_status( $current_days, $previous_days, $window_days ) {
        $current_days  = (int) $current_days;
        $previous_days = (int) $previous_days;
        $window_days   = max( 1, (int) $window_days );

        if ( $current_days <= 0 ) {
            return 'empty';
        }
        if ( $current_days < $window_days || $previous_days <= 0 ) {
            return 'partial';
        }
        return 'full';
    }
}
