<?php
/**
 * Settings/action audit trail — network-wide table, blog_id-scoped like the
 * other 6 plugin tables. Records *what* changed and *who* changed it, never
 * a raw diff of the values themselves (keeps the PII footprint small: a
 * short human-readable summary, not a serialized before/after blob).
 *
 * Same shape as Seoforge_Reports: a pure, testable core (build_entry(),
 * cutoff_timestamp()) with the $wpdb side effects kept in thin wrappers
 * (log(), cleanup(), query(), count()) around it.
 *
 * @package Brainwerk SEO Suite
 */

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

class Seoforge_Audit {

    const RETENTION_DEFAULT = 180;

    /** Rows the Free-build teaser shows before the Pro upsell. */
    const FREE_TEASER_LIMIT = 5;

    public static function register( Seoforge_Loader $loader ) {
        $loader->add_action( 'seoforge_cleanup_visits', __CLASS__, 'cleanup' );
    }

    /**
     * Build one audit row. Pure — no $wpdb, no direct calls to
     * get_current_user_id()/current_time(); the caller (log()) fills in
     * those environment values via $context so this stays unit-testable
     * without WordPress loaded.
     *
     * @param string $action  Short string key, e.g. 'settings_saved',
     *                        'redirect_deleted', 'license_activated'.
     * @param array  $context blog_id, user_id, user_login, object_type,
     *                        object_id, summary, created_at (all optional).
     * @return array Row ready for $wpdb->insert() (minus the id column).
     */
    public static function build_entry( $action, array $context = array() ) {
        $action = preg_replace( '/[^a-z0-9_]/', '', strtolower( (string) $action ) );
        $action = '' !== $action ? substr( $action, 0, 60 ) : 'unknown';

        $object_type = isset( $context['object_type'] ) && '' !== $context['object_type']
            ? substr( (string) $context['object_type'], 0, 40 )
            : null;
        $object_id = isset( $context['object_id'] ) && '' !== $context['object_id']
            ? substr( (string) $context['object_id'], 0, 60 )
            : null;
        $summary = isset( $context['summary'] ) ? substr( (string) $context['summary'], 0, 255 ) : '';

        return array(
            'blog_id'     => isset( $context['blog_id'] ) ? (int) $context['blog_id'] : 0,
            'user_id'     => isset( $context['user_id'] ) ? (int) $context['user_id'] : 0,
            'user_login'  => isset( $context['user_login'] ) ? substr( (string) $context['user_login'], 0, 60 ) : '',
            'action'      => $action,
            'object_type' => $object_type,
            'object_id'   => $object_id,
            'summary'     => $summary,
            'created_at'  => isset( $context['created_at'] ) ? (string) $context['created_at'] : gmdate( 'Y-m-d H:i:s' ),
        );
    }

    /**
     * UTC cutoff timestamp for "older than $days" retention deletes. Pure —
     * takes "now" as a parameter instead of calling time() so it's testable.
     */
    public static function cutoff_timestamp( $days, $now ) {
        $days = max( 1, (int) $days );
        return gmdate( 'Y-m-d H:i:s', (int) $now - DAY_IN_SECONDS * $days );
    }

    /**
     * Record one audit entry. Fills in the current user/site/time as
     * defaults, then inserts. $context can override any of build_entry()'s
     * keys (used by contexts without a "current user", e.g. uninstall.php
     * — though uninstall itself never has a live audit row possible on a
     * dropped table, this keeps the method usable from any admin flow).
     */
    public static function log( $action, array $context = array() ) {
        global $wpdb;
        $entry = self::build_entry( $action, self::fill_defaults( $context ) );
        $wpdb->insert( Seoforge_Utils::table( 'audit' ), $entry );
        return (int) $wpdb->insert_id;
    }

    private static function fill_defaults( array $context ) {
        if ( ! isset( $context['blog_id'] ) ) {
            $context['blog_id'] = Seoforge_Utils::current_blog_id();
        }
        if ( ! isset( $context['user_id'] ) || ! isset( $context['user_login'] ) ) {
            $user = function_exists( 'wp_get_current_user' ) ? wp_get_current_user() : null;
            if ( ! isset( $context['user_id'] ) ) {
                $context['user_id'] = ( $user && $user->exists() ) ? (int) $user->ID : 0;
            }
            if ( ! isset( $context['user_login'] ) ) {
                $context['user_login'] = ( $user && $user->exists() ) ? (string) $user->user_login : '';
            }
        }
        if ( ! isset( $context['created_at'] ) ) {
            $context['created_at'] = current_time( 'mysql', 1 );
        }
        return $context;
    }

    /**
     * Cleanup cron — drop entries older than the retention setting, scoped
     * to one blog (defaults to the current one) so a Multisite cleanup
     * never touches another site's rows — see
     * Seoforge_Tools::cleanup_now().
     */
    public static function cleanup( $blog_id = null ) {
        global $wpdb;
        $blog_id = null === $blog_id ? Seoforge_Utils::current_blog_id() : (int) $blog_id;
        $days   = max( 1, (int) Seoforge_Settings::get( 'audit_retention_days', self::RETENTION_DEFAULT ) );
        $cutoff = self::cutoff_timestamp( $days, time() );
        return $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE blog_id = %d AND created_at < %s", Seoforge_Utils::table( 'audit' ), $blog_id, $cutoff ) );
    }

    /**
     * Paginated read. $blog_id: an int to scope to one site, or null for
     * the whole network (network-admin overview) — mirrors
     * Seoforge_Utils::stats_scope()'s null-means-network convention.
     */
    public static function query( array $args = array() ) {
        global $wpdb;
        $table   = Seoforge_Utils::table( 'audit' );
        $limit   = isset( $args['limit'] ) ? max( 1, min( 500, (int) $args['limit'] ) ) : 20;
        $offset  = isset( $args['offset'] ) ? max( 0, (int) $args['offset'] ) : 0;
        $blog_id = array_key_exists( 'blog_id', $args ) ? $args['blog_id'] : Seoforge_Utils::current_blog_id();

        if ( null === $blog_id ) {
            $sql = $wpdb->prepare(
                "SELECT * FROM %i ORDER BY created_at DESC, id DESC LIMIT %d OFFSET %d",
                $table, $limit, $offset
            );
        } else {
            $sql = $wpdb->prepare(
                "SELECT * FROM %i WHERE blog_id = %d ORDER BY created_at DESC, id DESC LIMIT %d OFFSET %d",
                $table, (int) $blog_id, $limit, $offset
            );
        }
        return (array) $wpdb->get_results( $sql, ARRAY_A );
    }

    /**
     * Total row count. Same convention as query(): pass array('blog_id' =>
     * null) for the whole network; omitting 'blog_id' scopes to the current
     * site (Seoforge_Utils::current_blog_id()).
     */
    public static function count( array $args = array() ) {
        global $wpdb;
        $table   = Seoforge_Utils::table( 'audit' );
        $blog_id = array_key_exists( 'blog_id', $args ) ? $args['blog_id'] : Seoforge_Utils::current_blog_id();
        if ( null === $blog_id ) {
            return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i", $table ) );
        }
        return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE blog_id = %d", $table, (int) $blog_id ) );
    }
}
