<?php
/**
 * Persistencia compartida de tablas de conversión / cálculo.
 *
 * @package Logistica
 */

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

/**
 * Guarda el estado de las hojas tipo Excel en la base de datos.
 *
 * Seguridad:
 * - Los guardados parciales (una pestaña abierta) se fusionan con hojas existentes.
 * - Un payload vacío no puede borrar datos ya guardados.
 * - Filas quitadas con `row_id` van a papelera; cambios relevantes a versiones.
 */
class Logistica_Conversion_Tables {
	const OPTION = 'logistica_conversion_tables_v1';

	const NONCE = 'logistica_conversion_tables';

	const MODULE = 'conversion_tables';

	const LAST_VERSION_OPTION = 'logistica_conversion_tables_last_version_at';

	/**
	 * @var array<int, string>
	 */
	private static $allowed_types = array(
		'tapas-poliestireno',
		'bolsas-calculo',
		'bobina-pliego',
		'gramaje-papel',
		'calibre-material',
		'montaje-compartido',
		'pliego-conversion',
	);

	/**
	 * @return bool
	 */
	public static function user_can_access() {
		if ( ! Logistica_Roles::user_can_access_plugin() ) {
			return false;
		}

		$tabs = array(
			'logistica-conversiones',
			'logistica-calculo-tapas',
			'logistica-calculo-bolsas',
			'logistica-calculo-plastico',
		);

		foreach ( $tabs as $tab ) {
			if ( Logistica_Roles::user_can_tab( $tab, 'backend' ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @return array<string, mixed>
	 */
	public static function get_state() {
		$saved = get_option( self::OPTION, array() );

		if ( ! is_array( $saved ) ) {
			return array();
		}

		if ( isset( $saved['sheets'] ) && is_array( $saved['sheets'] ) ) {
			return self::sanitize_state( $saved['sheets'] );
		}

		return self::sanitize_state( $saved );
	}

	/**
	 * Guarda hojas enviadas por el cliente fusionándolas con las ya persistidas.
	 *
	 * @param array<string, mixed> $state Raw state from client (puede ser parcial).
	 * @return bool
	 */
	public static function save_state( $state ) {
		if ( ! is_array( $state ) ) {
			return false;
		}

		$incoming = self::sanitize_state( $state );
		$existing = self::get_state();

		// Nunca permitir que un payload vacío borre hojas con datos.
		if ( empty( $incoming ) && ! empty( $existing ) ) {
			return true;
		}

		$merged = $existing;
		foreach ( $incoming as $type => $sheet ) {
			$merged[ $type ] = $sheet;
		}

		$merged = self::ensure_row_ids( $merged );

		if ( self::state_fingerprint( $merged ) === self::state_fingerprint( $existing ) ) {
			return true;
		}

		self::soft_delete_missing_rows( $existing, $incoming );
		self::record_lifecycle_change( $existing, $merged );

		return self::write_sheets( $merged );
	}

	/**
	 * Reemplazo completo (solo restauración / migraciones controladas).
	 *
	 * @param array<string, mixed> $state Estado completo de hojas.
	 * @return bool
	 */
	public static function replace_state( $state ) {
		if ( ! is_array( $state ) ) {
			return false;
		}

		$clean = self::ensure_row_ids( self::sanitize_state( $state ) );
		return self::write_sheets( $clean );
	}

	/**
	 * @param array<string, mixed> $sheets Hojas sanitizadas.
	 * @return bool
	 */
	private static function write_sheets( $sheets ) {
		$payload = array(
			'version'    => 1,
			'updated_at' => current_time( 'mysql' ),
			'updated_by' => get_current_user_id(),
			'sheets'     => $sheets,
		);

		return update_option( self::OPTION, $payload, false );
	}

	/**
	 * @param array<string, mixed> $state State payload.
	 * @return array<string, mixed>
	 */
	private static function sanitize_state( $state ) {
		$clean = array();

		foreach ( $state as $type => $sheet ) {
			$type = sanitize_key( (string) $type );
			if ( ! in_array( $type, self::$allowed_types, true ) || ! is_array( $sheet ) ) {
				continue;
			}

			$rows       = isset( $sheet['rows'] ) && is_array( $sheet['rows'] ) ? $sheet['rows'] : array();
			$clean_rows = array();

			foreach ( $rows as $row ) {
				if ( ! is_array( $row ) ) {
					continue;
				}

				$clean_row = array();
				foreach ( $row as $key => $value ) {
					$field = sanitize_key( (string) $key );
					if ( '' === $field ) {
						continue;
					}
					$clean_row[ $field ] = is_scalar( $value )
						? sanitize_text_field( (string) $value )
						: '';
				}
				$clean_rows[] = $clean_row;
			}

			$entry = array(
				'type' => $type,
				'rows' => $clean_rows,
			);

			if ( isset( $sheet['settings'] ) && is_array( $sheet['settings'] ) ) {
				$settings = array();
				foreach ( $sheet['settings'] as $key => $value ) {
					$setting_key = preg_replace( '/[^a-z0-9_\-]/', '', (string) $key );
					if ( '' === $setting_key ) {
						continue;
					}
					$settings[ $setting_key ] = is_scalar( $value )
						? sanitize_text_field( (string) $value )
						: '';
				}
				$entry['settings'] = $settings;
			}

			$clean[ $type ] = $entry;
		}

		return $clean;
	}

	/**
	 * @param array<string, mixed> $state Hojas.
	 * @return array<string, mixed>
	 */
	private static function ensure_row_ids( $state ) {
		foreach ( $state as $type => $sheet ) {
			if ( empty( $sheet['rows'] ) || ! is_array( $sheet['rows'] ) ) {
				continue;
			}

			foreach ( $sheet['rows'] as $index => $row ) {
				if ( ! is_array( $row ) ) {
					continue;
				}
				if ( empty( $row['row_id'] ) ) {
					$state[ $type ]['rows'][ $index ]['row_id'] = self::generate_row_id();
				}
			}
		}

		return $state;
	}

	/**
	 * @return string
	 */
	public static function generate_row_id() {
		try {
			return 'ct_' . bin2hex( random_bytes( 8 ) );
		} catch ( Exception $e ) {
			return 'ct_' . uniqid( '', true );
		}
	}

	/**
	 * @param array<string, mixed> $state Hojas.
	 * @return string
	 */
	private static function state_fingerprint( $state ) {
		return md5( (string) wp_json_encode( $state ) );
	}

	/**
	 * @param array<string, mixed> $state Hojas.
	 * @return int
	 */
	private static function total_rows( $state ) {
		$total = 0;
		foreach ( $state as $sheet ) {
			$total += isset( $sheet['rows'] ) && is_array( $sheet['rows'] ) ? count( $sheet['rows'] ) : 0;
		}
		return $total;
	}

	/**
	 * Envía a papelera las filas con row_id presentes antes y ausentes en el guardado parcial.
	 *
	 * @param array<string, mixed> $existing Estado actual en BD.
	 * @param array<string, mixed> $incoming Hojas enviadas (solo las del DOM).
	 * @return void
	 */
	private static function soft_delete_missing_rows( $existing, $incoming ) {
		if ( ! class_exists( 'Logistica_Entity_Lifecycle', false ) ) {
			return;
		}

		foreach ( $incoming as $type => $sheet ) {
			$before_rows = isset( $existing[ $type ]['rows'] ) && is_array( $existing[ $type ]['rows'] )
				? $existing[ $type ]['rows']
				: array();
			$after_rows = isset( $sheet['rows'] ) && is_array( $sheet['rows'] ) ? $sheet['rows'] : array();

			$after_ids = array();
			foreach ( $after_rows as $row ) {
				$id = sanitize_text_field( (string) ( $row['row_id'] ?? '' ) );
				if ( '' !== $id ) {
					$after_ids[ $id ] = true;
				}
			}

			foreach ( $before_rows as $row ) {
				$row_id = sanitize_text_field( (string) ( $row['row_id'] ?? '' ) );
				if ( '' === $row_id || isset( $after_ids[ $row_id ] ) ) {
					continue;
				}

				$label = self::row_label( $type, $row );
				Logistica_Entity_Lifecycle::record_delete(
					self::MODULE,
					'sheet_row',
					$type . ':' . $row_id,
					array(
						'_storage'   => 'option',
						'sheet_type' => $type,
						'row'        => $row,
					),
					$label,
					sprintf(
						/* translators: %s: row label */
						__( 'Fila enviada a papelera: %s', 'logistica' ),
						$label
					)
				);
			}
		}
	}

	/**
	 * @param string               $type Tipo de hoja.
	 * @param array<string, mixed> $row  Fila.
	 * @return string
	 */
	private static function row_label( $type, $row ) {
		$product = trim( (string) ( $row['product'] ?? '' ) );
		$color   = trim( (string) ( $row['color'] ?? '' ) );
		$ref     = trim( (string) ( $row['referencia'] ?? $row['pedido'] ?? '' ) );

		$parts = array_filter( array( $product, $color, $ref ) );
		if ( ! empty( $parts ) ) {
			return implode( ' / ', $parts );
		}

		return sprintf(
			/* translators: %s: sheet type */
			__( 'Fila de %s', 'logistica' ),
			$type
		);
	}

	/**
	 * @param array<string, mixed> $before Estado anterior.
	 * @param array<string, mixed> $after  Estado nuevo.
	 * @return void
	 */
	private static function record_lifecycle_change( $before, $after ) {
		if ( ! class_exists( 'Logistica_Entity_Lifecycle', false ) ) {
			return;
		}

		$save_version = self::should_save_version( $before, $after );
		$summary      = self::change_summary( $before, $after );

		$payload_before = array(
			'_storage' => 'option',
			'sheets'   => $before,
		);
		$payload_after  = array(
			'_storage' => 'option',
			'sheets'   => $after,
		);

		if ( empty( $before ) ) {
			Logistica_Entity_Lifecycle::record_create(
				self::MODULE,
				'state',
				'v1',
				$payload_after,
				$summary
			);
			return;
		}

		Logistica_Entity_Lifecycle::record_update(
			self::MODULE,
			'state',
			'v1',
			$payload_before,
			$payload_after,
			$summary,
			$save_version
		);

		if ( $save_version ) {
			update_option( self::LAST_VERSION_OPTION, time(), false );
		}
	}

	/**
	 * @param array<string, mixed> $before Antes.
	 * @param array<string, mixed> $after  Después.
	 * @return bool
	 */
	private static function should_save_version( $before, $after ) {
		if ( self::total_rows( $after ) < self::total_rows( $before ) ) {
			return true;
		}

		foreach ( self::$allowed_types as $type ) {
			$before_has = isset( $before[ $type ] );
			$after_has  = isset( $after[ $type ] );
			if ( $before_has && ! $after_has ) {
				return true;
			}
			$b = count( $before[ $type ]['rows'] ?? array() );
			$a = count( $after[ $type ]['rows'] ?? array() );
			if ( $a < $b ) {
				return true;
			}
		}

		$last = (int) get_option( self::LAST_VERSION_OPTION, 0 );
		return ( time() - $last ) >= 300;
	}

	/**
	 * @param array<string, mixed> $before Antes.
	 * @param array<string, mixed> $after  Después.
	 * @return string
	 */
	private static function change_summary( $before, $after ) {
		$before_rows = self::total_rows( $before );
		$after_rows  = self::total_rows( $after );

		if ( $after_rows < $before_rows ) {
			return sprintf(
				/* translators: 1: before count, 2: after count */
				__( 'Tablas de cálculo: %1$d → %2$d filas (respaldo previo)', 'logistica' ),
				$before_rows,
				$after_rows
			);
		}

		return sprintf(
			/* translators: 1: before count, 2: after count */
			__( 'Tablas de cálculo actualizadas (%1$d → %2$d filas)', 'logistica' ),
			$before_rows,
			$after_rows
		);
	}

	/**
	 * Restaura una fila desde papelera o una versión del estado completo.
	 *
	 * @param string               $entity_type Tipo.
	 * @param string               $entity_id   ID.
	 * @param array<string, mixed> $payload     Snapshot.
	 * @return true|WP_Error
	 */
	public static function restore_from_payload( $entity_type, $entity_id, $payload ) {
		unset( $entity_id );

		if ( 'state' === $entity_type ) {
			$sheets = array();
			if ( isset( $payload['sheets'] ) && is_array( $payload['sheets'] ) ) {
				$sheets = $payload['sheets'];
			} elseif ( is_array( $payload ) ) {
				$sheets = $payload;
				unset( $sheets['_storage'] );
			}

			if ( empty( $sheets ) ) {
				return new WP_Error( 'empty_state', __( 'La versión no contiene hojas para restaurar.', 'logistica' ) );
			}

			if ( ! self::replace_state( $sheets ) ) {
				return new WP_Error( 'restore_failed', __( 'No se pudo restaurar el estado de tablas.', 'logistica' ) );
			}

			return true;
		}

		if ( 'sheet_row' === $entity_type ) {
			$sheet_type = sanitize_key( (string) ( $payload['sheet_type'] ?? '' ) );
			$row        = isset( $payload['row'] ) && is_array( $payload['row'] ) ? $payload['row'] : null;

			if ( ! in_array( $sheet_type, self::$allowed_types, true ) || ! $row ) {
				return new WP_Error( 'invalid_row', __( 'Payload de fila no válido.', 'logistica' ) );
			}

			$state = self::get_state();
			if ( ! isset( $state[ $sheet_type ] ) || ! is_array( $state[ $sheet_type ] ) ) {
				$state[ $sheet_type ] = array(
					'type' => $sheet_type,
					'rows' => array(),
				);
			}

			$row_id = sanitize_text_field( (string) ( $row['row_id'] ?? '' ) );
			if ( '' === $row_id ) {
				$row['row_id'] = self::generate_row_id();
				$row_id        = $row['row_id'];
			}

			$rows = isset( $state[ $sheet_type ]['rows'] ) && is_array( $state[ $sheet_type ]['rows'] )
				? $state[ $sheet_type ]['rows']
				: array();

			foreach ( $rows as $existing_row ) {
				if ( (string) ( $existing_row['row_id'] ?? '' ) === $row_id ) {
					return true;
				}
			}

			$rows[]                         = $row;
			$state[ $sheet_type ]['rows']   = $rows;
			$state[ $sheet_type ]['type']   = $sheet_type;

			if ( ! self::write_sheets( self::ensure_row_ids( self::sanitize_state( $state ) ) ) ) {
				return new WP_Error( 'restore_failed', __( 'No se pudo restaurar la fila.', 'logistica' ) );
			}

			return true;
		}

		return new WP_Error( 'unsupported', __( 'Restauración no soportada para este tipo.', 'logistica' ) );
	}
}
