<?php
/**
 * Pre-launch sanity check — walks through every module and reports
 * misconfiguration / hygiene issues that would embarrass a reviewer or
 * trip up a customer.
 *
 * Each check returns a row:
 *   [ id, label, status (ok|warn|fail), message, fix_url (optional) ]
 *
 * Currently only run from the Tools tab admin UI; no WP-CLI command wraps
 * it yet. No side effects — read-only.
 *
 * @package Brainwerk SEO Suite
 */

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

class Seoforge_Sanity {

    const STATUS_OK   = 'ok';
    const STATUS_WARN = 'warn';
    const STATUS_FAIL = 'fail';

    /**
     * Run all checks. Returns ordered array of result rows.
     */
    public static function run_all() {
        $results = array();

        // === Versioning + migration ===
        $results[] = self::check_version_constants();
        $results[] = self::check_db_migration();

        // === Cron health ===
        $results[] = self::check_cron_scheduled( 'seoforge_aggregate_sessions', __( 'Hourly session aggregation', 'brainwerk-seo-suite' ) );
        $results[] = self::check_cron_scheduled( 'seoforge_cleanup_visits', __( 'Daily visit/404 cleanup', 'brainwerk-seo-suite' ) );
        $results[] = self::check_cron_scheduled( 'seoforge_weekly_digest', __( 'Weekly e-mail digest', 'brainwerk-seo-suite' ) );
        $results[] = self::check_cron_scheduled( 'seoforge_daily_digest', __( 'Daily e-mail digest', 'brainwerk-seo-suite' ) );

        // === Database tables ===
        $results[] = self::check_db_tables();

        // === Privacy / hygiene ===
        $results[] = self::check_ip_anonymization();
        $results[] = self::check_retention_window( 'visit_retention_days', 365, __( 'Visit retention window', 'brainwerk-seo-suite' ) );
        $results[] = self::check_retention_window( 'session_retention_days', 730, __( 'Session retention window', 'brainwerk-seo-suite' ) );
        $results[] = self::check_retention_window( 'log_404_retention_days', 90, __( '404-log retention window', 'brainwerk-seo-suite' ) );

        // === Tracking + sitemap ===
        $results[] = self::check_tracking_active();
        $results[] = self::check_sitemap_reachable();

        // === Coexistence ===
        $results[] = self::check_coexistence();

        // === Freemius / licensing ===
        $results[] = self::check_freemius();

        // Re-use the existing Health module's setup-completeness tasks. Health
        // exposes check() → ['score'=>.., 'tasks'=>[{label,done,action_url,hint}]],
        // not a run()/severity-shaped result — mapped to the sanity row shape below.
        if ( class_exists( 'Seoforge_Health' ) && method_exists( 'Seoforge_Health', 'check' ) ) {
            $health = Seoforge_Health::check();
            $tasks  = is_array( $health ) && isset( $health['tasks'] ) ? (array) $health['tasks'] : array();
            foreach ( $tasks as $f ) {
                if ( ! is_array( $f ) ) {
                    continue;
                }
                $results[] = array(
                    'id'      => 'health_' . md5( (string) ( $f['label'] ?? '' ) ),
                    'label'   => $f['label'] ?? __( 'Health finding', 'brainwerk-seo-suite' ),
                    'status'  => ! empty( $f['done'] ) ? self::STATUS_OK : self::STATUS_WARN,
                    'message' => $f['hint'] ?? '',
                    'fix_url' => $f['action_url'] ?? null,
                );
            }
        }

        // === File hygiene ===
        $results[] = self::check_no_test_fixtures();

        // === wp.org submission readiness ===
        $results[] = self::check_readme_stable_tag();
        $results[] = self::check_translation_files_present();

        return array_values( array_filter( $results ) );
    }

    public static function summary() {
        $rows   = self::run_all();
        $totals = array( 'ok' => 0, 'warn' => 0, 'fail' => 0 );
        foreach ( $rows as $r ) {
            $status = $r['status'] ?? 'ok';
            if ( ! isset( $totals[ $status ] ) ) {
                $totals[ $status ] = 0;
            }
            $totals[ $status ]++;
        }
        return $totals;
    }

    /* ==================================================================
     *  Individual checks
     * ================================================================== */

    private static function check_version_constants() {
        $header_version = '';
        if ( function_exists( 'get_plugin_data' ) ) {
            $data           = get_plugin_data( SEOFORGE_FILE, false, false );
            $header_version = $data['Version'] ?? '';
        }
        $const = defined( 'SEOFORGE_VERSION' ) ? SEOFORGE_VERSION : '';
        if ( $header_version && $const && $header_version !== $const ) {
            return array(
                'id'      => 'version_constant_mismatch',
                'label'   => __( 'Plugin-header version vs. SEOFORGE_VERSION constant', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_FAIL,
                'message' => sprintf(
                    /* translators: 1: header version, 2: constant version */
                    __( 'Header says %1$s but constant is %2$s. Re-run bin/bump-version to re-sync.', 'brainwerk-seo-suite' ),
                    $header_version,
                    $const
                ),
                'fix_url' => null,
            );
        }
        return array(
            'id'      => 'version_constant_match',
            'label'   => __( 'Plugin-header version vs. SEOFORGE_VERSION constant', 'brainwerk-seo-suite' ),
            'status'  => self::STATUS_OK,
            'message' => sprintf(
                /* translators: %s: plugin version string (e.g. 0.5.0) */
                __( 'Both at %s.', 'brainwerk-seo-suite' ),
                $const ?: '?'
            ),
            'fix_url' => null,
        );
    }

    private static function check_db_migration() {
        $stored = (string) ( is_multisite()
            ? get_site_option( SEOFORGE_DB_VERSION_KEY, '0' )
            : get_option( SEOFORGE_DB_VERSION_KEY, '0' ) );
        $target = defined( 'SEOFORGE_DB_VERSION' ) ? SEOFORGE_DB_VERSION : '0';
        if ( version_compare( $stored, $target, '<' ) ) {
            return array(
                'id'      => 'db_migration_pending',
                'label'   => __( 'Database schema migration pending', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_WARN,
                'message' => sprintf(
                    /* translators: 1: stored DB version, 2: target version */
                    __( 'DB at v%1$s, code expects v%2$s. Visit any wp-admin page to trigger the migration.', 'brainwerk-seo-suite' ),
                    $stored,
                    $target
                ),
                'fix_url' => admin_url(),
            );
        }
        return array(
            'id'      => 'db_migration_ok',
            'label'   => __( 'Database schema migration', 'brainwerk-seo-suite' ),
            'status'  => self::STATUS_OK,
            'message' => sprintf(
                /* translators: %s: target database schema version */
                __( 'DB schema at v%s — current.', 'brainwerk-seo-suite' ),
                $target
            ),
            'fix_url' => null,
        );
    }

    private static function check_cron_scheduled( $hook, $label ) {
        $next = wp_next_scheduled( $hook );
        if ( $next ) {
            return array(
                'id'      => 'cron_' . $hook,
                'label'   => $label,
                'status'  => self::STATUS_OK,
                'message' => sprintf(
                    /* translators: %s: human-readable time until the next cron run (e.g. "5 mins") */
                    __( 'Next run in %s.', 'brainwerk-seo-suite' ),
                    human_time_diff( time(), (int) $next )
                ),
                'fix_url' => null,
            );
        }
        return array(
            'id'      => 'cron_' . $hook,
            'label'   => $label,
            'status'  => self::STATUS_WARN,
            'message' => __( 'Cron hook not scheduled. Deactivate + reactivate plugin to repair.', 'brainwerk-seo-suite' ),
            'fix_url' => admin_url( 'plugins.php' ),
        );
    }

    private static function check_db_tables() {
        global $wpdb;
        $required = array(
            $wpdb->base_prefix . 'seoforge_visits',
            $wpdb->base_prefix . 'seoforge_sessions',
            $wpdb->base_prefix . 'seoforge_redirects',
            $wpdb->base_prefix . 'seoforge_404',
            $wpdb->base_prefix . 'seoforge_meta',
            $wpdb->base_prefix . 'seoforge_daily',
            $wpdb->base_prefix . 'seoforge_audit',
        );
        $missing = array();
        foreach ( $required as $t ) {
            $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) );
            if ( $exists !== $t ) {
                $missing[] = $t;
            }
        }
        if ( $missing ) {
            return array(
                'id'      => 'db_tables_missing',
                'label'   => __( 'Required database tables', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_FAIL,
                'message' => sprintf(
                    /* translators: %s: comma-separated list of missing database table names */
                    __( 'Missing: %s. Reactivate plugin to recreate.', 'brainwerk-seo-suite' ),
                    implode( ', ', $missing )
                ),
                'fix_url' => admin_url( 'plugins.php' ),
            );
        }
        return array(
            'id'      => 'db_tables_ok',
            'label'   => __( 'Required database tables', 'brainwerk-seo-suite' ),
            'status'  => self::STATUS_OK,
            'message' => sprintf(
                /* translators: %d: number of required database tables */
                __( 'All %d core tables present.', 'brainwerk-seo-suite' ),
                count( $required )
            ),
            'fix_url' => null,
        );
    }

    private static function check_ip_anonymization() {
        $on = (bool) Seoforge_Settings::get( 'privacy_anonymize_ip', true );
        return array(
            'id'      => 'privacy_anon_ip',
            'label'   => __( 'IP anonymization (GDPR)', 'brainwerk-seo-suite' ),
            'status'  => $on ? self::STATUS_OK : self::STATUS_FAIL,
            'message' => $on
                ? __( 'On — IPv4 truncated to /24, IPv6 to /64.', 'brainwerk-seo-suite' )
                : __( 'Off — full IP addresses are stored. Under GDPR you almost certainly need explicit user consent and a privacy-policy section that reflects this.', 'brainwerk-seo-suite' ),
            'fix_url' => admin_url( 'admin.php?page=brainwerk-seo-suite&tab=privacy' ),
        );
    }

    private static function check_retention_window( $key, $default, $label ) {
        $days = (int) Seoforge_Settings::get( $key, $default );
        if ( $days < 1 ) {
            return array(
                'id'      => 'retention_' . $key . '_invalid',
                'label'   => $label,
                'status'  => self::STATUS_FAIL,
                'message' => __( 'Retention is 0 — data would be wiped on the next cleanup cron.', 'brainwerk-seo-suite' ),
                'fix_url' => admin_url( 'admin.php?page=brainwerk-seo-suite&tab=privacy' ),
            );
        }
        if ( $days > 3650 ) {
            return array(
                'id'      => 'retention_' . $key . '_long',
                'label'   => $label,
                'status'  => self::STATUS_WARN,
                'message' => sprintf(
                    /* translators: %d: days */
                    __( '%d days is unusually long. GDPR data-minimization principles suggest the shortest period that still meets your business need.', 'brainwerk-seo-suite' ),
                    $days
                ),
                'fix_url' => admin_url( 'admin.php?page=brainwerk-seo-suite&tab=privacy' ),
            );
        }
        return array(
            'id'      => 'retention_' . $key . '_ok',
            'label'   => $label,
            'status'  => self::STATUS_OK,
            'message' => sprintf(
                /* translators: %d: configured retention period in days */
                __( '%d days — sensible.', 'brainwerk-seo-suite' ),
                $days
            ),
            'fix_url' => null,
        );
    }

    private static function check_tracking_active() {
        $on   = (bool) Seoforge_Settings::get( 'tracking_enabled', true );
        $mode = (string) Seoforge_Settings::get( 'tracking_mode', 'server' );
        if ( ! $on ) {
            return array(
                'id'      => 'tracking_off',
                'label'   => __( 'Visitor tracking', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_WARN,
                'message' => __( 'Tracking is disabled. The Analytics tabs will stay empty.', 'brainwerk-seo-suite' ),
                'fix_url' => admin_url( 'admin.php?page=brainwerk-seo-suite&tab=privacy' ),
            );
        }
        return array(
            'id'      => 'tracking_on',
            'label'   => __( 'Visitor tracking', 'brainwerk-seo-suite' ),
            'status'  => self::STATUS_OK,
            'message' => sprintf(
                /* translators: %s: tracking mode identifier (e.g. "server" or "js") */
                __( 'Active (mode: %s).', 'brainwerk-seo-suite' ),
                $mode
            ),
            'fix_url' => null,
        );
    }

    private static function check_sitemap_reachable() {
        $enabled = (bool) Seoforge_Settings::get( 'sitemap_enabled', true );
        if ( ! $enabled ) {
            return array(
                'id'      => 'sitemap_off',
                'label'   => __( 'XML sitemap', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_OK,
                'message' => __( 'Disabled in settings — skipped.', 'brainwerk-seo-suite' ),
                'fix_url' => null,
            );
        }

        // Skip the network round-trip when we are running in CLI/cron context;
        // a 127.0.0.1 self-fetch from there would falsely report failure.
        if ( defined( 'WP_CLI' ) || defined( 'DOING_CRON' ) || PHP_SAPI === 'cli' ) {
            return array(
                'id'      => 'sitemap_skipped_cli',
                'label'   => __( 'XML sitemap', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_OK,
                'message' => __( 'Skipped in CLI context (would self-fetch 127.0.0.1). Re-run from the Tools tab to verify.', 'brainwerk-seo-suite' ),
                'fix_url' => null,
            );
        }

        $url = home_url( '/seoforge-sitemap.xml' );
        $res = wp_remote_get(
            $url,
            array(
                'timeout'     => 5,
                'redirection' => 2,
                'sslverify'   => false,
                'user-agent'  => 'Brainwerk SEO Suite-Sanity/' . SEOFORGE_VERSION,
            )
        );
        if ( is_wp_error( $res ) ) {
            return array(
                'id'      => 'sitemap_unreachable',
                'label'   => __( 'XML sitemap', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_WARN,
                'message' => sprintf(
                    /* translators: 1: sitemap URL, 2: WP_Error message from the failed HTTP request */
                    __( 'Could not fetch %1$s: %2$s', 'brainwerk-seo-suite' ),
                    $url,
                    $res->get_error_message()
                ),
                'fix_url' => admin_url( 'admin.php?page=brainwerk-seo-suite&tab=sitemap' ),
            );
        }
        $code = (int) wp_remote_retrieve_response_code( $res );
        $body = (string) wp_remote_retrieve_body( $res );
        if ( 200 !== $code || strpos( $body, '<?xml' ) === false ) {
            return array(
                'id'      => 'sitemap_bad_response',
                'label'   => __( 'XML sitemap', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_WARN,
                'message' => sprintf(
                    /* translators: 1: HTTP status code returned by the sitemap, 2: sitemap URL */
                    __( 'Sitemap returned HTTP %1$d (expected 200 + XML). URL: %2$s. Try saving Settings once to flush rewrite rules.', 'brainwerk-seo-suite' ),
                    $code,
                    $url
                ),
                'fix_url' => admin_url( 'admin.php?page=brainwerk-seo-suite&tab=sitemap' ),
            );
        }
        return array(
            'id'      => 'sitemap_ok',
            'label'   => __( 'XML sitemap', 'brainwerk-seo-suite' ),
            'status'  => self::STATUS_OK,
            'message' => sprintf(
                /* translators: %s: sitemap URL that responded with HTTP 200 + XML */
                __( 'Reachable at %s.', 'brainwerk-seo-suite' ),
                $url
            ),
            'fix_url' => null,
        );
    }

    private static function check_coexistence() {
        if ( ! class_exists( 'Seoforge_Compat' ) ) {
            return null;
        }
        $det      = Seoforge_Compat::detect_competitor();
        $force    = (bool) Seoforge_Settings::get( 'seo_force_takeover', false );
        if ( ! empty( $det['active'] ) && $force ) {
            return array(
                'id'      => 'coexistence_force_conflict',
                'label'   => __( 'Coexistence with other SEO plugin', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_WARN,
                'message' => sprintf(
                    /* translators: %s: competitor plugin name */
                    __( '%s is active AND seo_force_takeover is on — duplicate meta/canonical/sitemap output will likely confuse search engines. Pick one.', 'brainwerk-seo-suite' ),
                    $det['name']
                ),
                'fix_url' => admin_url( 'admin.php?page=brainwerk-seo-suite&tab=privacy' ),
            );
        }
        if ( ! empty( $det['active'] ) ) {
            return array(
                'id'      => 'coexistence_passive',
                'label'   => __( 'Coexistence with other SEO plugin', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_OK,
                'message' => sprintf(
                    /* translators: %s: competitor plugin name */
                    __( '%s detected — Brainwerk SEO Suite stands down on meta/schema/sitemap and runs Analytics + 404 + Redirects + Analyzer alongside.', 'brainwerk-seo-suite' ),
                    $det['name']
                ),
                'fix_url' => null,
            );
        }
        return array(
            'id'      => 'coexistence_solo',
            'label'   => __( 'Coexistence with other SEO plugin', 'brainwerk-seo-suite' ),
            'status'  => self::STATUS_OK,
            'message' => __( 'No competing SEO plugin detected — Brainwerk SEO Suite handles meta + schema + sitemap.', 'brainwerk-seo-suite' ),
            'fix_url' => null,
        );
    }

    private static function check_freemius() {
        $sdk_path = SEOFORGE_PATH . 'freemius/start.php';
        if ( ! file_exists( $sdk_path ) ) {
            return array(
                'id'      => 'freemius_sdk_missing',
                'label'   => __( 'Freemius SDK', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_WARN,
                'message' => __( 'freemius/start.php not present — plugin runs in free-tier-only mode. See FREEMIUS_SETUP.md.', 'brainwerk-seo-suite' ),
                'fix_url' => null,
            );
        }
        if ( ! function_exists( 'seoforge_fs' ) ) {
            return array(
                'id'      => 'freemius_bootstrap_missing',
                'label'   => __( 'Freemius bootstrap', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_FAIL,
                'message' => __( 'seoforge_fs() not defined — includes/class-seoforge-freemius.php is missing or failed to load.', 'brainwerk-seo-suite' ),
                'fix_url' => null,
            );
        }
        $fs = seoforge_fs();
        if ( ! $fs ) {
            return array(
                'id'      => 'freemius_init_failed',
                'label'   => __( 'Freemius initialization', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_FAIL,
                'message' => __( 'seoforge_fs() returned null — check the product ID + public_key in class-seoforge-freemius.php.', 'brainwerk-seo-suite' ),
                'fix_url' => null,
            );
        }
        if ( method_exists( $fs, 'is_live' ) && ! $fs->is_live() ) {
            return array(
                'id'      => 'freemius_dev_mode',
                'label'   => __( 'Freemius mode', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_WARN,
                'message' => __( 'SDK runs with is_live=false — admin menus + checkout are suppressed. Flip to true in class-seoforge-freemius.php before going live.', 'brainwerk-seo-suite' ),
                'fix_url' => null,
            );
        }
        if ( ! $fs->is_registered() ) {
            return array(
                'id'      => 'freemius_not_registered',
                'label'   => __( 'Freemius opt-in', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_OK,
                'message' => __( 'Site has not yet opted in. Click "Allow & Continue" on the Freemius banner to enable license-based features.', 'brainwerk-seo-suite' ),
                'fix_url' => admin_url(),
            );
        }
        if ( ! $fs->is_paying() ) {
            return array(
                'id'      => 'freemius_free_plan',
                'label'   => __( 'Freemius license', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_OK,
                'message' => __( 'Opted in on the Free plan — Pro modules are off. Upgrade on the License tab to unlock them.', 'brainwerk-seo-suite' ),
                'fix_url' => admin_url( 'admin.php?page=brainwerk-seo-suite&tab=license' ),
            );
        }
        $plan       = method_exists( $fs, 'get_plan' ) ? $fs->get_plan() : null;
        $plan_name  = $plan && isset( $plan->name ) ? strtolower( $plan->name ) : 'pro';
        $license    = method_exists( $fs, '_get_license' ) ? $fs->_get_license() : null;
        $expiration = $license && isset( $license->expiration ) ? (string) $license->expiration : '';
        $msg        = sprintf(
            /* translators: 1: plan name (e.g. "agency"), 2: human-readable expiration date or "n/a" */
            __( 'Active %1$s license — renews %2$s.', 'brainwerk-seo-suite' ),
            ucfirst( $plan_name ),
            $expiration ? mysql2date( get_option( 'date_format' ), $expiration ) : __( 'n/a', 'brainwerk-seo-suite' )
        );
        return array(
            'id'      => 'freemius_pro_active',
            'label'   => __( 'Freemius license', 'brainwerk-seo-suite' ),
            'status'  => self::STATUS_OK,
            'message' => $msg,
            'fix_url' => null,
        );
    }

    private static function check_no_test_fixtures() {
        $needles = array(
            WP_PLUGIN_DIR . '/seoforge-test-fixture.php',
            WP_PLUGIN_DIR . '/sf-test-fixture.php',
            WP_PLUGIN_DIR . '/test-fixture.php',
        );
        $found = array();
        foreach ( $needles as $p ) {
            if ( file_exists( $p ) ) {
                $found[] = basename( $p );
            }
        }
        if ( $found ) {
            return array(
                'id'      => 'test_fixtures_present',
                'label'   => __( 'Test fixtures in plugins directory', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_FAIL,
                'message' => sprintf(
                    /* translators: %s: comma-separated list of test fixture filenames found in the plugins directory */
                    __( 'Found: %s. Delete before going live.', 'brainwerk-seo-suite' ),
                    implode( ', ', $found )
                ),
                'fix_url' => null,
            );
        }
        return array(
            'id'      => 'no_test_fixtures',
            'label'   => __( 'Test fixtures', 'brainwerk-seo-suite' ),
            'status'  => self::STATUS_OK,
            'message' => __( 'No leftover test files in the plugins directory.', 'brainwerk-seo-suite' ),
            'fix_url' => null,
        );
    }

    private static function check_readme_stable_tag() {
        $path = SEOFORGE_PATH . 'readme.txt';
        if ( ! file_exists( $path ) ) {
            return array(
                'id'      => 'readme_missing',
                'label'   => __( 'wp.org readme.txt', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_FAIL,
                'message' => __( 'readme.txt is missing — wp.org requires it.', 'brainwerk-seo-suite' ),
                'fix_url' => null,
            );
        }
        $content = (string) file_get_contents( $path );
        if ( preg_match( '/^Stable tag:\s*(\S+)/m', $content, $m ) ) {
            $tag = trim( $m[1] );
            if ( defined( 'SEOFORGE_VERSION' ) && $tag !== SEOFORGE_VERSION ) {
                return array(
                    'id'      => 'readme_stable_tag_mismatch',
                    'label'   => __( 'readme.txt Stable tag', 'brainwerk-seo-suite' ),
                    'status'  => self::STATUS_WARN,
                    'message' => sprintf(
                        /* translators: 1: readme tag, 2: plugin version */
                        __( 'Stable tag is %1$s but plugin is %2$s. Bump readme before tagging the wp.org release.', 'brainwerk-seo-suite' ),
                        $tag,
                        SEOFORGE_VERSION
                    ),
                    'fix_url' => null,
                );
            }
            return array(
                'id'      => 'readme_stable_tag_ok',
                'label'   => __( 'readme.txt Stable tag', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_OK,
                'message' => sprintf(
                    /* translators: %s: readme.txt Stable tag value matching SEOFORGE_VERSION */
                    __( 'Matches plugin version (%s).', 'brainwerk-seo-suite' ),
                    $tag
                ),
                'fix_url' => null,
            );
        }
        return array(
            'id'      => 'readme_no_stable_tag',
            'label'   => __( 'readme.txt Stable tag', 'brainwerk-seo-suite' ),
            'status'  => self::STATUS_WARN,
            'message' => __( 'No "Stable tag:" line found in readme.txt.', 'brainwerk-seo-suite' ),
            'fix_url' => null,
        );
    }

    private static function check_translation_files_present() {
        $langdir  = SEOFORGE_PATH . 'languages/';
        $required = array( 'seoforge.pot', 'seoforge-de_DE.po', 'seoforge-de_DE.mo' );
        $missing  = array();
        foreach ( $required as $f ) {
            if ( ! file_exists( $langdir . $f ) ) {
                $missing[] = $f;
            }
        }
        if ( $missing ) {
            return array(
                'id'      => 'translations_missing',
                'label'   => __( 'Translation files', 'brainwerk-seo-suite' ),
                'status'  => self::STATUS_WARN,
                'message' => sprintf(
                    /* translators: %s: comma-separated list of missing translation files (e.g. "seoforge.pot, seoforge-de_DE.mo") */
                    __( 'Missing: %s. Run wp i18n make-pot + ship a de_DE.po/.mo.', 'brainwerk-seo-suite' ),
                    implode( ', ', $missing )
                ),
                'fix_url' => null,
            );
        }
        return array(
            'id'      => 'translations_present',
            'label'   => __( 'Translation files', 'brainwerk-seo-suite' ),
            'status'  => self::STATUS_OK,
            'message' => __( 'POT + DE PO/MO files all present in languages/.', 'brainwerk-seo-suite' ),
            'fix_url' => null,
        );
    }

    /* ==================================================================
     *  Helpers
     * ================================================================== */

    private static function map_severity_to_status( $severity ) {
        $sev = (string) $severity;
        if ( in_array( $sev, array( 'critical', 'high', 'fail', 'error' ), true ) ) {
            return self::STATUS_FAIL;
        }
        if ( in_array( $sev, array( 'warn', 'warning', 'medium' ), true ) ) {
            return self::STATUS_WARN;
        }
        return self::STATUS_OK;
    }
}
