<?php

declare(strict_types=1);

/**
 * IssueUIGroupingService - UI Grouping for Scanner Issue Cards
 *
 * Extracted from IssueRepository to isolate the UI grouping logic.
 * Groups enriched vulnerability and content issues into synthetic cards
 * for display in the scanner results panel.
 *
 */

namespace SiteFort\Modules\Scanner\Repository;

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

class IssueUIGroupingService {

    private const UI_GROUP_FETCH_CAP = 2000;

    /**
     * Group enriched issues into UI cards and apply pagination.
     *
     * @param array $issues  Enriched issues
     * @param int   $limit
     * @param int   $offset
     * @return array
     */
    public static function group_and_slice( array $issues, int $limit, int $offset ): array {
        $grouped = self::group_issues_for_ui( $issues );
        $sliced = array_slice( $grouped, max( 0, $offset ), $limit > 0 ? $limit : null );
        return array_values( $sliced );
    }

    /**
     * Resolve how many rows to fetch before UI grouping.
     *
     * @param int $limit
     * @param int $offset
     * @return int
     */
    public static function resolve_fetch_limit( int $limit, int $offset ): int {
        $target = max( 200, ( $offset + $limit + 20 ) * 5 );
        return min( self::UI_GROUP_FETCH_CAP, $target );
    }

    /**
     * Resolve the raw-row chunk size used while building a grouped UI page.
     *
     * @param int $limit
     * @return int
     */
    public static function resolve_fetch_chunk_size( int $limit ): int {
        return max( 75, min( 200, max( 1, $limit ) * 2 ) );
    }

    /**
     * Determine whether the grouped collection already contains the requested page.
     *
     * @param array $issues Enriched issues fetched so far.
     * @param int   $limit
     * @param int   $offset
     * @return bool
     */
    public static function has_enough_for_page( array $issues, int $limit, int $offset ): bool {
        $required = max( 0, $offset ) + max( 1, $limit );
        return self::count_grouped_cards( $issues ) >= $required;
    }

    /**
     * Count grouped UI cards without building synthetic card DTOs.
     *
     * @param array $issues
     * @return int
     */
    private static function count_grouped_cards( array $issues ): int {
        $seen = [];
        foreach ( $issues as $issue ) {
            if ( ! is_array( $issue ) ) {
                continue;
            }

            $bucket = self::resolve_ui_bucket_key( $issue );
            $bucket_key = $bucket['bucket'] . '|' . $bucket['key'];
            $seen[ $bucket_key ] = true;
        }

        return count( $seen );
    }

    /**
     * Group vulnerability and content rows into synthetic cards.
     *
     * @param array $issues
     * @return array
     */
    private static function group_issues_for_ui( array $issues ): array {
        $buckets = [];
        $order = [];

        foreach ( $issues as $issue ) {
            if ( ! is_array( $issue ) ) {
                continue;
            }

            $bucket = self::resolve_ui_bucket_key( $issue );
            $bucket_key = $bucket['bucket'] . '|' . $bucket['key'];

            if ( ! isset( $buckets[ $bucket_key ] ) ) {
                $buckets[ $bucket_key ] = [
                    'bucket' => $bucket['bucket'],
                    'key'    => $bucket['key'],
                    'issues' => [],
                ];
                $order[] = $bucket_key;
            }

            $buckets[ $bucket_key ]['issues'][] = $issue;
        }

        $output = [];
        foreach ( $order as $bucket_key ) {
            $bucket = $buckets[ $bucket_key ];
            $bucket_issues = $bucket['issues'];
            if ( empty( $bucket_issues ) ) {
                continue;
            }

            if ( $bucket['bucket'] === 'vulnerability' ) {
                $output[] = self::build_vulnerability_group_issue( $bucket_issues, $bucket['key'] );
                continue;
            }

            if ( $bucket['bucket'] === 'content' ) {
                if ( count( $bucket_issues ) === 1 ) {
                    $output[] = $bucket_issues[0];
                } else {
                    $output[] = self::build_content_group_issue( $bucket_issues, $bucket['key'] );
                }
                continue;
            }

            $output[] = $bucket_issues[0];
        }

        return $output;
    }

    /**
     * Resolve grouping bucket for a UI card.
     *
     * @param array $issue
     * @return array{bucket:string,key:string}
     */
    private static function resolve_ui_bucket_key( array $issue ): array {
        $type = sanitize_key( strtolower( (string) ( $issue['type'] ?? '' ) ) );
        $status = sanitize_key( strtolower( (string) ( $issue['status'] ?? '' ) ) );

        if (
            $type === 'vulnerability' ||
            $type === 'vulnerable_plugin' ||
            $status === 'vulnerable'
        ) {
            $data = ( isset( $issue['data'] ) && is_array( $issue['data'] ) ) ? $issue['data'] : [];
            $component_type = sanitize_key( (string) ( $data['component_type'] ?? '' ) );
            if ( ! in_array( $component_type, [ 'plugin', 'theme', 'core' ], true ) ) {
                $component_type = ( $type === 'theme' || $type === 'core' ) ? $type : 'plugin';
            }
            $slug = sanitize_title_with_dashes(
                (string) (
                    $issue['slug']
                    ?? $data['slug']
                    ?? $issue['file']
                    ?? $issue['name']
                    ?? $issue['id']
                    ?? 'unknown_asset'
                )
            );
            if ( $slug === '' ) {
                $slug = 'unknown_asset';
            }

            return [
                'bucket' => 'vulnerability',
                'key' => $component_type . ':' . strtolower( $slug ),
            ];
        }

        if ( $type === 'content' ) {
            $data = ( isset( $issue['data'] ) && is_array( $issue['data'] ) ) ? $issue['data'] : [];
            $table = sanitize_key( (string) ( $data['table'] ?? 'unknown' ) );
            $row_id = (string) ( $data['row_id'] ?? $data['post_id'] ?? $data['comment_id'] ?? '' );
            if ( $row_id !== '' ) {
                return [
                    'bucket' => 'content',
                    'key' => strtolower( $table . ':' . $row_id ),
                ];
            }
        }

        $hash = (string) ( $issue['hash'] ?? $issue['issue_hash'] ?? $issue['id'] ?? uniqid( 'issue_', true ) );
        return [
            'bucket' => 'single',
            'key' => strtolower( $hash ),
        ];
    }

    /**
     * Build a synthetic vulnerability group card.
     *
     * @param array  $issues
     * @param string $group_key
     * @return array
     */
    private static function build_vulnerability_group_issue( array $issues, string $group_key ): array {
        $sorted_children = self::sort_issues_for_group( $issues );
        $sorted_children = array_map(
            static function ( array $child ): array {
                $child_data = ( isset( $child['data'] ) && is_array( $child['data'] ) ) ? $child['data'] : [];
                if ( isset( $child_data['can_update'] ) && is_bool( $child_data['can_update'] ) ) {
                    $child['can_update'] = $child_data['can_update'];
                }
                return $child;
            },
            $sorted_children
        );

        $highest = $sorted_children[0];
        $highest_data = ( isset( $highest['data'] ) && is_array( $highest['data'] ) ) ? $highest['data'] : [];

        $asset_type = sanitize_key( (string) ( $highest_data['component_type'] ?? '' ) );
        if ( ! in_array( $asset_type, [ 'plugin', 'theme', 'core' ], true ) ) {
            $asset_type = 'plugin';
        }

        $asset_slug = sanitize_key(
            (string) (
                $highest['slug']
                ?? $highest_data['slug']
                ?? explode( ':', $group_key, 2 )[1]
                ?? 'unknown_asset'
            )
        );
        if ( $asset_slug === '' ) {
            $asset_slug = 'unknown_asset';
        }

        $theme_active_flag = null;
        if ( $asset_type === 'theme' ) {
            $theme_active_flag = false;
            foreach ( $sorted_children as $child ) {
                $child_data = ( isset( $child['data'] ) && is_array( $child['data'] ) ) ? $child['data'] : [];
                if ( ! empty( $child_data['is_active'] ) ) {
                    $theme_active_flag = true;
                    break;
                }
            }
        }

        $asset_name = trim(
            (string) (
                $highest_data['component_name']
                ?? $highest_data['plugin_name']
                ?? $highest_data['theme_name']
                ?? $highest_data['asset_name']
                ?? $highest['name']
                ?? ''
            )
        );

        $synthetic = $highest;
        $synthetic['id'] = 'group-' . substr( md5( $group_key ), 0, 16 );
        $synthetic['hash'] = IssueRepository::generateHash( 'vulnerability_group', $group_key );
        $synthetic['type'] = 'vulnerability_group';
        $synthetic['status'] = 'vulnerable';
        $synthetic['severity'] = (string) ( $highest['severity'] ?? 'medium' );
        // Group is ignored only when ALL children are ignored.
        // Using "any" would hide active threats behind ignored ones.
        $synthetic['is_ignored'] = ! empty( $sorted_children ) && empty(
            array_filter(
                $sorted_children,
                static fn( $child ) => empty( $child['is_ignored'] )
            )
        );
        $synthetic['meta'] = [
            'layout' => 'vulnerability_card',
            'label' => 'Vulnerability',
            'issues' => array_map( [ self::class, 'compact_vulnerability_child_issue' ], $sorted_children ),
            'asset_slug' => $asset_slug,
            'asset_type' => $asset_type,
            'asset_name' => $asset_name,
            'asset_version' => (string) ( $highest['version'] ?? '' ),
            'highest_severity' => (string) ( $highest['severity'] ?? 'medium' ),
            'is_active' => $theme_active_flag,
            'remediation_preferred' => (string) ( $highest_data['remediation_preferred'] ?? '' ),
        ];

        // Rebuild display and action_policy for the synthetic group card so
        // universal actions (ignore, mark-as-fixed) are present on grouped cards.
        unset( $synthetic['display'], $synthetic['action_policy'] );
        $synthetic['display']       = IssueDisplayContractBuilder::build_display_contract( $synthetic );
        $synthetic['action_policy'] = IssueDisplayContractBuilder::build_action_policy( $synthetic );

        return $synthetic;
    }

    /**
     * Build a synthetic content group card.
     *
     * @param array  $issues
     * @param string $group_key
     * @return array
     */
    private static function build_content_group_issue( array $issues, string $group_key ): array {
        $sorted_children = self::sort_issues_for_group( $issues );
        $highest = $sorted_children[0];
        $highest_data = ( isset( $highest['data'] ) && is_array( $highest['data'] ) ) ? $highest['data'] : [];

        $synthetic = $highest;
        $synthetic['id'] = 'content-group-' . substr( md5( $group_key ), 0, 16 );
        $synthetic['hash'] = IssueRepository::generateHash( 'content_group', $group_key );
        $synthetic['type'] = 'content_group';
        $synthetic['severity'] = (string) ( $highest['severity'] ?? 'medium' );
        $synthetic['meta'] = [
            'layout' => 'content_group_card',
            'label' => 'Content Safety',
            'issues' => array_map( [ self::class, 'compact_content_child_issue' ], $sorted_children ),
            'table' => (string) ( $highest_data['table'] ?? '' ),
            'row_id' => (string) ( $highest_data['row_id'] ?? $highest_data['post_id'] ?? $highest_data['comment_id'] ?? '' ),
            'post_title' => (string) ( $highest_data['post_title'] ?? $highest_data['option_name'] ?? '' ),
            'post_url' => (string) ( $highest_data['post_url'] ?? '' ),
            'edit_url' => (string) ( $highest_data['edit_url'] ?? '' ),
        ];

        // Rebuild display and action_policy for the synthetic group card so
        // universal actions (ignore, mark-as-fixed) are present on grouped cards.
        unset( $synthetic['display'], $synthetic['action_policy'] );
        $synthetic['display']       = IssueDisplayContractBuilder::build_display_contract( $synthetic );
        $synthetic['action_policy'] = IssueDisplayContractBuilder::build_action_policy( $synthetic );

        return $synthetic;
    }

    /**
     * Keep grouped vulnerability child rows limited to fields used by the UI and
     * action targets. Parent cards retain the full backend-issued display/action contract.
     *
     * @param array $child
     * @return array
     */
    private static function compact_vulnerability_child_issue( array $child ): array {
        $compact = self::copy_issue_fields(
            $child,
            [
                'id',
                'hash',
                'issue_hash',
                'scan_id',
                'scan_run_id',
                'type',
                'status',
                'severity',
                'file',
                'file_path',
                'message',
                'details',
                'name',
                'title',
                'slug',
                'version',
                'vnv',
                'cve',
                'cvss',
                'fixed_in',
                'description',
                'is_ignored',
                'can_update',
                'created_at',
                'updated_at',
            ]
        );

        $data = self::copy_issue_data_fields(
            $child,
            [
                'component_type',
                'component_name',
                'plugin_name',
                'theme_name',
                'asset_name',
                'slug',
                'asset_slug',
                'is_active',
                'can_update',
                'remediation_preferred',
            ]
        );

        if ( ! empty( $data ) ) {
            $compact['data'] = $data;
        }

        return $compact;
    }

    /**
     * Keep grouped content child rows limited to expanded-row evidence and action targets.
     *
     * @param array $child
     * @return array
     */
    private static function compact_content_child_issue( array $child ): array {
        $compact = self::copy_issue_fields(
            $child,
            [
                'id',
                'hash',
                'issue_hash',
                'scan_id',
                'scan_run_id',
                'type',
                'status',
                'severity',
                'file',
                'file_path',
                'message',
                'details',
                'match',
                'is_ignored',
                'created_at',
                'updated_at',
            ]
        );

        $data = self::copy_issue_data_fields(
            $child,
            [
                'table',
                'row_id',
                'post_id',
                'comment_id',
                'option_name',
                'post_title',
                'post_url',
                'edit_url',
                'signature_desc',
                'match_snippet',
                'match',
                'snippet',
                'excerpt',
                'matched_text',
                'matchedText',
            ]
        );

        if ( ! empty( $data ) ) {
            $compact['data'] = $data;
        }

        return $compact;
    }

    /**
     * @param array<int,string> $fields
     */
    private static function copy_issue_fields( array $issue, array $fields ): array {
        $copy = [];
        foreach ( $fields as $field ) {
            if ( array_key_exists( $field, $issue ) ) {
                $copy[ $field ] = $issue[ $field ];
            }
        }

        return $copy;
    }

    /**
     * @param array<int,string> $fields
     */
    private static function copy_issue_data_fields( array $issue, array $fields ): array {
        $source = ( isset( $issue['data'] ) && is_array( $issue['data'] ) ) ? $issue['data'] : [];
        $copy = [];
        foreach ( $fields as $field ) {
            if ( array_key_exists( $field, $source ) ) {
                $copy[ $field ] = $source[ $field ];
            }
        }

        return $copy;
    }

    /**
     * Sort issues using UI severity/date ordering.
     *
     * @param array $issues
     * @return array
     */
    private static function sort_issues_for_group( array $issues ): array {
        usort(
            $issues,
            static function ( array $a, array $b ): int {
                $a_rank = self::severity_rank( (string) ( $a['severity'] ?? '' ) );
                $b_rank = self::severity_rank( (string) ( $b['severity'] ?? '' ) );
                if ( $a_rank !== $b_rank ) {
                    return $b_rank <=> $a_rank;
                }

                $a_created = (int) ( $a['created_at'] ?? 0 );
                $b_created = (int) ( $b['created_at'] ?? 0 );
                return $b_created <=> $a_created;
            }
        );

        return $issues;
    }

    /**
     * Normalize severity rank for sorting.
     *
     * @param string $severity
     * @return int
     */
    private static function severity_rank( string $severity ): int {
        $severity = sanitize_key( strtolower( $severity ) );
        switch ( $severity ) {
            case 'critical':
                return 4;
            case 'high':
                return 3;
            case 'medium':
                return 2;
            case 'low':
                return 1;
            default:
                return 0;
        }
    }
}
