<?php
/**
 * Extension Series Store
 *
 * A modular series store that can be used by any extension (presupuestos, albaranes, etc.)
 * to manage their own numbering series. This is designed to be extension-agnostic,
 * meaning if an extension is removed, its series are handled independently.
 *
 * @package Freefactu
 * @since 1.2.0
 */

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

/**
 * Class Freefactu_Extension_Series_Store
 *
 * Handles CRUD operations for extension series and number generation.
 * Each extension has its own series stored in a shared table with type prefixing.
 */
class Freefactu_Extension_Series_Store {

	/** @var string Table name with WordPress prefix */
	private static $table_name = null;

	/**
	 * Get the series table name with WordPress prefix.
	 *
	 * @return string Table name with prefix.
	 */
	public static function get_table_name() {
		global $wpdb;
		if ( self::$table_name === null ) {
			self::$table_name = $wpdb->prefix . 'freefactu_extension_series';
		}
		return self::$table_name;
	}

	/**
	 * Check if the series table exists.
	 *
	 * @return bool True if table exists, false otherwise.
	 */
	public static function table_exists() {
		global $wpdb;
		$table_name = self::get_table_name();
		return $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) ) === $table_name; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	/**
	 * Create the series database table.
	 *
	 * @return void
	 */
	public static function create_table() {
		global $wpdb;
		$table_name = self::get_table_name();
		$charset_collate = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE $table_name (
			id bigint(20) NOT NULL AUTO_INCREMENT,
			extension_id varchar(50) NOT NULL,
			name varchar(100) NOT NULL,
			prefix varchar(50) DEFAULT '' NOT NULL,
			suffix varchar(50) DEFAULT '' NOT NULL,
			padding int(2) DEFAULT 0 NOT NULL,
			next_number bigint(20) DEFAULT 1 NOT NULL,
			is_active tinyint(1) DEFAULT 0 NOT NULL,
			created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
			PRIMARY KEY  (id),
			KEY extension_id (extension_id),
			KEY is_active (is_active)
		) $charset_collate;";

		require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
		dbDelta( $sql );
	}

	/**
	 * Create a new series for an extension.
	 *
	 * @param string $extension_id Extension identifier (e.g., 'presupuestos').
	 * @param array  $data {
	 *     @type string $name         Display name for the series.
	 *     @type string $prefix       Number prefix.
	 *     @type string $suffix       Number suffix.
	 *     @type int    $padding      Zero padding length.
	 *     @type int    $next_number  Starting number (default: 1).
	 * }
	 * @return int|WP_Error Insert ID on success, WP_Error on failure.
	 */
	public static function create_series( $extension_id, $data ) {
		global $wpdb;
		
		// Ensure table exists
		if ( ! self::table_exists() ) {
			self::create_table();
		}
		
		$table_name = self::get_table_name();

		// Validate required fields
		if ( empty( $extension_id ) || empty( $data['name'] ) ) {
			return new WP_Error( 'missing_required_fields', __( 'Extension ID and name are required.', 'freefactu' ) );
		}

		// Check for duplicate series (same prefix + suffix + extension)
		$prefix = isset( $data['prefix'] ) ? sanitize_text_field( $data['prefix'] ) : '';
		$suffix = isset( $data['suffix'] ) ? sanitize_text_field( $data['suffix'] ) : '';
		if ( self::series_exists( $extension_id, $prefix, $suffix ) ) {
			return new WP_Error( 
				'duplicate_series', 
				__( 'Ya existe una serie con el mismo prefijo y sufijo.', 'freefactu' ) 
			);
		}

		// Check if this is the first series - if so, make it active
		$existing_count = self::count_series( $extension_id );
		$is_active = $existing_count === 0 ? 1 : 0;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result = $wpdb->insert(
			$table_name,
			array(
				'extension_id' => sanitize_key( $extension_id ),
				'name'         => sanitize_text_field( $data['name'] ),
				'prefix'       => $prefix,
				'suffix'       => $suffix,
				'padding'      => isset( $data['padding'] ) ? absint( $data['padding'] ) : 4,
				'next_number'  => isset( $data['next_number'] ) ? absint( $data['next_number'] ) : 1,
				'is_active'    => $is_active,
				'created_at'   => current_time( 'mysql' ),
			),
			array( '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s' )
		);

		// Clear cache
		wp_cache_delete( 'list_' . $extension_id, 'freefactu_series' );

		if ( false === $result ) {
			$error_msg = $wpdb->last_error ? $wpdb->last_error : 'Unknown database error';
			Freefactu_Logger::log(
				sprintf( 'Failed to create extension series "%s" for %s: %s', $data['name'], $extension_id, $error_msg ),
				'error'
			);
			return new WP_Error( 'db_insert_error', __( 'Failed to create series.', 'freefactu' ), array( 'wpdb_error' => $error_msg ) );
		}

		$series_id = $wpdb->insert_id;

		Freefactu_Logger::log(
			sprintf( 'Created new extension series: %s (ID: %d, Extension: %s, Active: %s)', $data['name'], $series_id, $extension_id, $is_active ? 'Yes' : 'No' ),
			'info'
		);

		return $series_id;
	}

	/**
	 * Get series by ID.
	 *
	 * @param int $id Series ID.
	 * @return object|null Series object or null if not found.
	 */
	public static function get_series( $id ) {
		global $wpdb;

		$cached = wp_cache_get( $id, 'freefactu_series' );
		if ( false !== $cached ) {
			return $cached;
		}

		$table_name = self::get_table_name();
		
		if ( ! self::table_exists() ) {
			return null;
		}
		
		$series = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter

		if ( $series ) {
			wp_cache_set( $id, $series, 'freefactu_series' );
		}

		return $series;
	}

	/**
	 * Get all series for an extension.
	 *
	 * @param string $extension_id Extension identifier.
	 * @return array Array of series objects.
	 */
	public static function get_series_by_extension( $extension_id ) {
		global $wpdb;

		$cache_key = 'list_' . $extension_id;
		$cached = wp_cache_get( $cache_key, 'freefactu_series' );
		if ( false !== $cached ) {
			return $cached;
		}

		$table_name = self::get_table_name();
		
		if ( ! self::table_exists() ) {
			return array();
		}
		
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$results = $wpdb->get_results( $wpdb->prepare(
			"SELECT * FROM $table_name WHERE extension_id = %s ORDER BY is_active DESC, created_at ASC",
			$extension_id
		) );
		// phpcs:enable

		if ( $results ) {
			wp_cache_set( $cache_key, $results, 'freefactu_series' );
		}

		return $results;
	}

	/**
	 * Get the active series for an extension.
	 *
	 * @param string $extension_id Extension identifier.
	 * @return object|null Active series object or null if none active.
	 */
	public static function get_active_series( $extension_id ) {
		$all_series = self::get_series_by_extension( $extension_id );
		foreach ( $all_series as $series ) {
			if ( (int) $series->is_active === 1 ) {
				return $series;
			}
		}
		return null;
	}

	/**
	 * Set a series as active (deactivates others of the same extension).
	 *
	 * @param int $series_id Series ID to activate.
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public static function set_active_series( $series_id ) {
		global $wpdb;
		$table_name = self::get_table_name();

		// Get series to validate and determine extension
		$series = self::get_series( $series_id );
		if ( ! $series ) {
			return new WP_Error( 'series_not_found', __( 'Series not found.', 'freefactu' ) );
		}
		
		// To invalidate cache later
		$extension_id = $series->extension_id;
		$all_extension_series = self::get_series_by_extension( $extension_id );

		// Start transaction
		$wpdb->query( 'START TRANSACTION' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery

		// Deactivate all series of the same extension
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result1 = $wpdb->update(
			$table_name,
			array( 'is_active' => 0 ),
			array( 'extension_id' => $series->extension_id ),
			array( '%d' ),
			array( '%s' )
		);

		// Activate the specified series
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result2 = $wpdb->update(
			$table_name,
			array( 'is_active' => 1 ),
			array( 'id' => $series_id ),
			array( '%d' ),
			array( '%d' )
		);

		if ( false === $result1 || false === $result2 ) {
			$wpdb->query( 'ROLLBACK' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			return new WP_Error( 'db_update_error', __( 'Failed to update active series.', 'freefactu' ) );
		}

		$wpdb->query( 'COMMIT' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery

		// Clear caches
		wp_cache_delete( 'list_' . $extension_id, 'freefactu_series' );
		if ( ! empty( $all_extension_series ) ) {
			foreach ( $all_extension_series as $s ) {
				wp_cache_delete( $s->id, 'freefactu_series' );
			}
		}

		Freefactu_Logger::log(
			sprintf( 'Activated extension series ID %d (%s) for extension: %s', $series_id, $series->name, $series->extension_id ),
			'info'
		);

		return true;
	}

	/**
	 * Count series for an extension.
	 *
	 * @param string $extension_id Extension identifier.
	 * @return int Count of series.
	 */
	public static function count_series( $extension_id ) {
		$series = self::get_series_by_extension( $extension_id );
		return count( $series );
	}

	/**
	 * Check if a series with the same prefix, suffix, and extension already exists.
	 *
	 * @param string $extension_id Extension identifier.
	 * @param string $prefix       Series prefix.
	 * @param string $suffix       Series suffix.
	 * @return bool True if exists, false otherwise.
	 */
	public static function series_exists( $extension_id, $prefix, $suffix ) {
		$series = self::get_series_by_extension( $extension_id );
		foreach ( $series as $s ) {
			if ( $s->prefix === $prefix && $s->suffix === $suffix ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Increment and return the next number for a series.
	 *
	 * This is an atomic operation that increments the counter and returns the value.
	 *
	 * @param int $series_id Series ID.
	 * @return int|WP_Error The number that was consumed, or WP_Error on failure.
	 */
	public static function increment( $series_id ) {
		global $wpdb;
		$table_name = self::get_table_name();

		// Lock the row for update
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$series = $wpdb->get_row( $wpdb->prepare(
			"SELECT * FROM $table_name WHERE id = %d FOR UPDATE",
			$series_id
		) );
		// phpcs:enable

		if ( ! $series ) {
			return new WP_Error( 'series_not_found', __( 'Series not found.', 'freefactu' ) );
		}

		$current_number = $series->next_number;

		// Increment the counter
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result = $wpdb->update(
			$table_name,
			array( 'next_number' => $current_number + 1 ),
			array( 'id' => $series_id ),
			array( '%d' ),
			array( '%d' )
		);

		if ( false === $result ) {
			return new WP_Error( 'db_update_error', __( 'Failed to increment series number.', 'freefactu' ) );
		}

		// Clear caches
		wp_cache_delete( $series_id, 'freefactu_series' );
		wp_cache_delete( 'list_' . $series->extension_id, 'freefactu_series' );

		return $current_number;
	}

	/**
	 * Get formatted number for a series.
	 *
	 * Applies prefix, suffix, padding, and date placeholders.
	 *
	 * @param object $series The series object.
	 * @param int    $number The raw number.
	 * @return string Formatted number.
	 */
	public static function format_number( $series, $number ) {
		$prefix = $series->prefix;
		$suffix = $series->suffix;
		$padding = (int) $series->padding;

		// Get current date for placeholders
		$date = new DateTime();

		// Replace date placeholders
		$replacements = array(
			'[year]'  => $date->format( 'Y' ),
			'[month]' => $date->format( 'm' ),
			'[day]'   => $date->format( 'd' ),
		);

		$prefix = str_replace( array_keys( $replacements ), array_values( $replacements ), $prefix );
		$suffix = str_replace( array_keys( $replacements ), array_values( $replacements ), $suffix );

		// Pad number
		$number_str = (string) $number;
		if ( $padding > 0 ) {
			$number_str = str_pad( $number_str, $padding, '0', STR_PAD_LEFT );
		}

		return $prefix . $number_str . $suffix;
	}

	/**
	 * Get preview of the next number for a series.
	 *
	 * @param object $series Series object.
	 * @return string Preview of the next formatted number.
	 */
	public static function get_preview( $series ) {
		return self::format_number( $series, $series->next_number );
	}

	/**
	 * Delete a series if it has no usage.
	 *
	 * @param int $series_id Series ID.
	 * @param int $usage_count How many items have been created with this series (from extension).
	 * @return bool|WP_Error True on success, WP_Error if series is in use or on failure.
	 */
	public static function delete_series( $series_id, $usage_count = null ) {
		global $wpdb;
		$table_name = self::get_table_name();

		$series = self::get_series( $series_id );
		if ( ! $series ) {
			return new WP_Error( 'series_not_found', __( 'Series not found.', 'freefactu' ) );
		}

		// If usage_count not provided, estimate from next_number
		if ( $usage_count === null ) {
			$usage_count = max( 0, (int) $series->next_number - 1 );
		}
		
		// Check if series has been used
		if ( $usage_count > 0 ) {
			return new WP_Error(
				'series_in_use',
				/* translators: %d: Number of issued items */
				sprintf( __( 'No se puede eliminar la serie con %d elementos emitidos.', 'freefactu' ), $usage_count )
			);
		}

		// Cannot delete active series if there are other series
		if ( $series->is_active && self::count_series( $series->extension_id ) > 1 ) {
			return new WP_Error( 'cannot_delete_active', __( 'No se puede eliminar la serie activa. Activa otra serie primero.', 'freefactu' ) );
		}

		$result = $wpdb->delete( $table_name, array( 'id' => $series_id ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery

		if ( false === $result ) {
			return new WP_Error( 'db_delete_error', __( 'Error al eliminar la serie.', 'freefactu' ) );
		}
		
		// Clear caches
		wp_cache_delete( $series_id, 'freefactu_series' );
		wp_cache_delete( 'list_' . $series->extension_id, 'freefactu_series' );

		Freefactu_Logger::log(
			sprintf( 'Deleted extension series ID %d (%s) for extension: %s', $series_id, $series->name, $series->extension_id ),
			'info'
		);

		return true;
	}

	/**
	 * Count usage for a series (estimated from next_number - 1).
	 *
	 * @param int $series_id Series ID.
	 * @return int Estimated usage count.
	 */
	public static function count_usage( $series_id ) {
		$series = self::get_series( $series_id );
		if ( ! $series ) {
			return 0;
		}
		
		return max( 0, (int) $series->next_number - 1 );
	}

	/**
	 * Create default series for an extension if none exist.
	 *
	 * @param string $extension_id Extension identifier.
	 * @param array  $defaults {
	 *     @type string $name    Default series name.
	 *     @type string $prefix  Default prefix.
	 *     @type string $suffix  Default suffix.
	 *     @type int    $padding Default padding.
	 * }
	 * @return int|WP_Error|null Series ID if created, null if already exists, WP_Error on failure.
	 */
	public static function ensure_default_series( $extension_id, $defaults = array() ) {
		// Check if we already have series for this extension
		if ( self::count_series( $extension_id ) > 0 ) {
			return null;
		}

		$defaults = wp_parse_args( $defaults, array(
			'name'    => __( 'Serie Principal', 'freefactu' ),
			'prefix'  => '',
			'suffix'  => '',
			'padding' => 4,
		) );

		return self::create_series( $extension_id, $defaults );
	}

	/**
	 * Delete all series for an extension (used during cleanup).
	 *
	 * @param string $extension_id Extension identifier.
	 * @return bool True on success.
	 */
	public static function delete_all_series( $extension_id ) {
		global $wpdb;
		$table_name = self::get_table_name();
		
		if ( ! self::table_exists() ) {
			return true;
		}
		
		// Get series to clear individual caches
		$all_series = self::get_series_by_extension( $extension_id );

		$result = $wpdb->delete( $table_name, array( 'extension_id' => $extension_id ), array( '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery

		if ( false !== $result ) {
			wp_cache_delete( 'list_' . $extension_id, 'freefactu_series' );
			if ( ! empty( $all_series ) ) {
				foreach ( $all_series as $s ) {
					wp_cache_delete( $s->id, 'freefactu_series' );
				}
			}
		}
		
		Freefactu_Logger::log(
			sprintf( 'Deleted all series for extension: %s', $extension_id ),
			'info'
		);
		
		return false !== $result;
	}
}
