<?php
/**
 * Usage statistics tracking for gamification dashboard.
 *
 * @package AgenticWP
 */

namespace Agentic_WP;

defined( 'ABSPATH' ) || exit;

/**
 * Tracks tool usage statistics for the gamification dashboard.
 *
 * @since 1.0.0
 */
class Usage_Stats {

	const OPTION_NAME          = 'agenticwp_usage_stats';
	const SCHEMA_VERSION       = 1;
	const DAILY_HISTORY_DAYS   = 30;
	const WEEKLY_HISTORY_WEEKS = 12;

	/** User levels indexed by minimum tool calls threshold. */
	private const LEVELS = array(
		0    => array(
			'level' => 1,
			'name'  => 'Beginner',
			'max'   => 49,
		),
		50   => array(
			'level' => 2,
			'name'  => 'Apprentice',
			'max'   => 149,
		),
		150  => array(
			'level' => 3,
			'name'  => 'Intermediate',
			'max'   => 299,
		),
		300  => array(
			'level' => 4,
			'name'  => 'Advanced',
			'max'   => 499,
		),
		500  => array(
			'level' => 5,
			'name'  => 'Expert',
			'max'   => 749,
		),
		750  => array(
			'level' => 6,
			'name'  => 'Master',
			'max'   => 999,
		),
		1000 => array(
			'level' => 7,
			'name'  => 'Legend',
			'max'   => PHP_INT_MAX,
		),
	);

	/** Agent type to stat key mapping. */
	private const AGENT_STAT_MAP = array(
		'creator'          => 'agent_creator_jobs',
		'editor'           => 'agent_editor_jobs',
		'interlinking'     => 'agent_interlinking_jobs',
		'meta_description' => 'agent_meta_jobs',
	);

	/**
	 * Singleton instance.
	 *
	 * @var self|null
	 */
	private static ?self $instance = null;

	/**
	 * Cached stats data.
	 *
	 * @var array|null
	 */
	private ?array $stats = null;

	/**
	 * Whether stats have been modified and need saving.
	 *
	 * @var bool
	 */
	private bool $dirty = false;

	/**
	 * Gets the singleton instance.
	 *
	 * @return self
	 */
	public static function get_instance(): self {
		return self::$instance ??= new self();
	}

	/**
	 * Private constructor to enforce singleton.
	 */
	private function __construct() {
		add_action( 'agentic_wp_after_tool_execution', array( $this, 'record_tool_call' ), 10, 3 );
		add_action( 'shutdown', array( $this, 'maybe_save' ) );
	}

	/**
	 * Records a tool call and updates relevant statistics.
	 *
	 * @param string $name   Tool name that was executed.
	 * @param array  $args   Tool arguments that were provided.
	 * @param string $result Tool execution result.
	 */
	public function record_tool_call( string $name, array $args, string $result ): void {
		if ( str_starts_with( $result, 'ERROR:' ) ) {
			return;
		}

		$stats = $this->get_stats();
		$today = gmdate( 'Y-m-d' );
		$week  = gmdate( 'Y-W' );

		// Initialize periods if needed.
		$default_period             = array(
			'tool_calls'    => 0,
			'posts_created' => 0,
		);
		$stats['daily'][ $today ] ??= $default_period;
		$stats['weekly'][ $week ] ??= $default_period;

		// Increment tool calls across all periods.
		++$stats['lifetime']['total_tool_calls'];
		++$stats['daily'][ $today ]['tool_calls'];
		++$stats['weekly'][ $week ]['tool_calls'];

		// Track tool-specific metrics.
		$this->track_tool_metrics( $name, $args, $stats, $today, $week );
		$this->update_streak( $stats, $today );
		$this->rotate_history( $stats );

		$stats['last_updated'] = time();
		$this->stats           = $stats;
		$this->dirty           = true;
	}

	/**
	 * Tracks metrics specific to each tool type.
	 *
	 * @param string $name   Tool name.
	 * @param array  $args   Tool arguments.
	 * @param array  $stats  Stats array (by reference).
	 * @param string $today  Today's date key.
	 * @param string $week   Current week key.
	 */
	private function track_tool_metrics( string $name, array $args, array &$stats, string $today, string $week ): void {
		switch ( $name ) {
			case 'start_post':
				++$stats['lifetime']['posts_created'];
				++$stats['daily'][ $today ]['posts_created'];
				++$stats['weekly'][ $week ]['posts_created'];
				break;

			case 'append_blocks_structured':
				$stats['lifetime']['blocks_appended'] += is_array( $args['blocks'] ?? null ) ? count( $args['blocks'] ) : 0;
				break;

			case 'replace_blocks_segment':
				++$stats['lifetime']['block_replacements'];
				++$stats['lifetime']['posts_edited'];
				break;

			case 'save_meta_description':
				++$stats['lifetime']['meta_descriptions'];
				break;

			case 'delegate_post_agent':
				$agent = sanitize_key( $args['agent'] ?? '' );
				if ( isset( self::AGENT_STAT_MAP[ $agent ] ) ) {
					++$stats['lifetime'][ self::AGENT_STAT_MAP[ $agent ] ];
				}
				break;

			case 'search_content':
				++$stats['lifetime']['content_searches'];
				break;
		}
	}

	/**
	 * Updates streak based on activity.
	 *
	 * @param array  $stats Stats array (by reference).
	 * @param string $today Today's date key.
	 */
	private function update_streak( array &$stats, string $today ): void {
		$last_active = $stats['streaks']['last_active_date'];
		if ( $today === $last_active ) {
			return;
		}

		$yesterday                            = gmdate( 'Y-m-d', strtotime( '-1 day' ) );
		$stats['streaks']['current_streak']   = ( $yesterday === $last_active ) ? $stats['streaks']['current_streak'] + 1 : 1;
		$stats['streaks']['longest_streak']   = max( $stats['streaks']['current_streak'], $stats['streaks']['longest_streak'] );
		$stats['streaks']['last_active_date'] = $today;
	}

	/**
	 * Rotates old history to maintain limits.
	 *
	 * @param array $stats Stats array (by reference).
	 */
	private function rotate_history( array &$stats ): void {
		foreach ( array(
			'daily'  => self::DAILY_HISTORY_DAYS,
			'weekly' => self::WEEKLY_HISTORY_WEEKS,
		) as $type => $limit ) {
			if ( count( $stats[ $type ] ) > $limit ) {
				krsort( $stats[ $type ] );
				$stats[ $type ] = array_slice( $stats[ $type ], 0, $limit, true );
			}
		}
	}

	/**
	 * Gets the current stats, loading from database if needed.
	 */
	public function get_stats(): array {
		if ( null !== $this->stats ) {
			return $this->stats;
		}

		$stored = get_option( self::OPTION_NAME, array() );
		if ( empty( $stored ) || ! isset( $stored['version'] ) ) {
			$this->stats = $this->get_default_stats();
			return $this->stats;
		}

		if ( $stored['version'] < self::SCHEMA_VERSION ) {
			$stored['version'] = self::SCHEMA_VERSION;
		}

		$this->stats = $stored;
		return $this->stats;
	}

	/**
	 * Gets default empty stats structure.
	 *
	 * @return array Default stats.
	 */
	private function get_default_stats(): array {
		return array(
			'version'      => self::SCHEMA_VERSION,
			'lifetime'     => array(
				'posts_created'           => 0,
				'pages_created'           => 0,
				'blocks_appended'         => 0,
				'posts_edited'            => 0,
				'block_replacements'      => 0,
				'meta_descriptions'       => 0,
				'agent_creator_jobs'      => 0,
				'agent_editor_jobs'       => 0,
				'agent_interlinking_jobs' => 0,
				'agent_meta_jobs'         => 0,
				'content_searches'        => 0,
				'total_tool_calls'        => 0,
				'images_generated'        => 0,
			),
			'daily'        => array(),
			'weekly'       => array(),
			'streaks'      => array(
				'current_streak'   => 0,
				'longest_streak'   => 0,
				'last_active_date' => '',
			),
			'achievements' => array(),
			'last_updated' => 0,
		);
	}

	/**
	 * Saves stats to database if modified.
	 */
	public function maybe_save(): void {
		if ( $this->dirty && null !== $this->stats ) {
			update_option( self::OPTION_NAME, $this->stats, false );
			$this->dirty = false;
		}
	}

	/**
	 * Forces immediate save of stats.
	 */
	public function save(): void {
		if ( null !== $this->stats ) {
			update_option( self::OPTION_NAME, $this->stats, false );
			$this->dirty = false;
		}
	}

	/**
	 * Gets a specific lifetime stat.
	 *
	 * @param string $key Stat key.
	 * @return int Stat value.
	 */
	public function get_lifetime_stat( string $key ): int {
		return (int) ( $this->get_stats()['lifetime'][ $key ] ?? 0 );
	}

	/**
	 * Gets all lifetime stats.
	 *
	 * @return array Lifetime stats.
	 */
	public function get_lifetime_stats(): array {
		return $this->get_stats()['lifetime'];
	}

	/**
	 * Gets daily stats for a specific date or all daily stats.
	 *
	 * @param string|null $date Optional specific date (Y-m-d format).
	 * @return array Daily stats.
	 */
	public function get_daily_stats( ?string $date = null ): array {
		$stats = $this->get_stats();
		if ( null === $date ) {
			return $stats['daily'];
		}
		return $stats['daily'][ $date ] ?? array(
			'tool_calls'    => 0,
			'posts_created' => 0,
		);
	}

	/**
	 * Gets weekly stats for a specific week or all weekly stats.
	 *
	 * @param string|null $week Optional specific week (Y-W format).
	 * @return array Weekly stats.
	 */
	public function get_weekly_stats( ?string $week = null ): array {
		$stats = $this->get_stats();
		if ( null === $week ) {
			return $stats['weekly'];
		}
		return $stats['weekly'][ $week ] ?? array(
			'tool_calls'    => 0,
			'posts_created' => 0,
		);
	}

	/**
	 * Gets streak information.
	 *
	 * @return array Streak data.
	 */
	public function get_streaks(): array {
		return $this->get_stats()['streaks'];
	}

	/**
	 * Gets current streak count.
	 *
	 * @return int Current streak.
	 */
	public function get_current_streak(): int {
		$streaks     = $this->get_streaks();
		$last_active = $streaks['last_active_date'];
		$today       = gmdate( 'Y-m-d' );
		$yesterday   = gmdate( 'Y-m-d', strtotime( '-1 day' ) );

		return ( $last_active === $today || $last_active === $yesterday ) ? (int) $streaks['current_streak'] : 0;
	}

	/**
	 * Gets achievements data.
	 *
	 * @return array Achievements with unlock timestamps.
	 */
	public function get_achievements(): array {
		return $this->get_stats()['achievements'];
	}

	/**
	 * Unlocks an achievement.
	 *
	 * @param string $achievement_id Achievement identifier.
	 * @return bool True if newly unlocked, false if already unlocked.
	 */
	public function unlock_achievement( string $achievement_id ): bool {
		$stats = $this->get_stats();
		if ( ! empty( $stats['achievements'][ $achievement_id ] ) ) {
			return false;
		}

		$stats['achievements'][ $achievement_id ] = time();
		$this->stats                              = $stats;
		$this->dirty                              = true;
		return true;
	}

	/**
	 * Checks if an achievement is unlocked.
	 *
	 * @param string $achievement_id Achievement identifier.
	 * @return bool True if unlocked.
	 */
	public function is_achievement_unlocked( string $achievement_id ): bool {
		return ! empty( $this->get_achievements()[ $achievement_id ] );
	}

	/**
	 * Calculates current user level based on total tool calls.
	 *
	 * @return array Level info with 'level', 'name', 'min', 'max'.
	 */
	public function get_user_level(): array {
		$total = $this->get_lifetime_stat( 'total_tool_calls' );

		// Find the highest level threshold the user has reached.
		$min = 0;
		foreach ( array_keys( self::LEVELS ) as $threshold ) {
			if ( $total >= $threshold ) {
				$min = $threshold;
			}
		}

		return array(
			'level' => self::LEVELS[ $min ]['level'],
			'name'  => self::LEVELS[ $min ]['name'],
			'min'   => $min,
			'max'   => self::LEVELS[ $min ]['max'],
		);
	}

	/**
	 * Gets total agent delegation jobs.
	 *
	 * @return int Total agent jobs.
	 */
	public function get_total_agent_jobs(): int {
		$lifetime = $this->get_stats()['lifetime'];
		return array_sum( array_intersect_key( $lifetime, array_flip( self::AGENT_STAT_MAP ) ) );
	}
}
