<?php

namespace JetBackup\Data;

use Exception;
use JetBackup\Factory;
use JetBackup\Log\LogController;
use Mysqldump\Mysqldump as MysqldumpAlias;
use PDO;
use PDOException;

if (!defined( '__JETBACKUP__')) die('Direct access is not allowed');

Class Mysqldump Extends MysqldumpAlias {

	private ArrayData $_data;
	private LogController $_logController;

	const QUERY_MAX_RETRIES = 10;
	const SQLSTATE_ERRORS = ['08S01', '08001', '40001', 'HY000'];

	/***
	 * These SQLSTATE errors indicate connection failures and trigger a retry.
	 * - '08S01' → Communication link failure (e.g., network disconnect, timeout, or dropped connection)
	 * - '08001' → Client unable to establish a connection (e.g., incorrect credentials, DNS issues)
	 * - '40001' → Transaction deadlock detected (e.g., deadlock errors requiring retry)
	 * - 'HY000' → General MySQL error (covers various issues, including MySQL server disconnects)
	 */


	/**
	 * @throws Exception
	 */
	public function __construct($db_name, $db_user, $db_password, $db_host) {

		$this->_data = new ArrayData();

		$this->_setDBName($db_name);
		$this->_setDBPassword($db_password);
		$this->_setDBUser($db_user);
		$this->_setDBHost($db_host);

		parent::__construct(
			'mysql:host='.$this->getDBHost().';port='.$this->getDBPort().';dbname='.$this->getDBName(), //dsn
			$this->getDBUser(),
			$this->getDBPassword(),
			[
				'compress' => MysqldumpAlias::NONE, // Always use NONE, we use our own internal gzip class
				'add-drop-table' => true, // Enable DROP TABLE IF EXISTS
				'if-not-exists' => true,
				'reset-auto-increment' => true,//resets the AUTO_INCREMENT value in tables to ensure consistency when restoring data.
				'complete-insert' => true,//  include column names in each INSERT statement
				'default-character-set' => MysqldumpAlias::UTF8MB4,
				'extended-insert' => false,// when false, each insert will be in a separate line helping for resuming import
				'insert-ignore' => true,
				'lock-tables' => false,// no need to lock if using single-transaction
			]
		);

	}

	public function setDumpSetting($key, $value):void { $this->dumpSettings[$key] = $value; }
	public function getDumpSetting($key, $default=null) { return $this->dumpSettings[$key] ?? $default; }
	
	public function set($key, $value){
		$this->_data->set($key, $value);
	}

	public function get($key, $default=''){
		return $this->_data->get($key, $default);
	}
	
	
	public function setLogController(LogController $log) {
		$this->_logController = $log;
	}

	private function getLogController(): LogController {
		if (!isset($this->_logController)) $this->_logController = new LogController();
		return $this->_logController;
	}


	private function _setDBHost($db_host) {
		if (strpos($db_host, ':') !== false) {
			$parts = explode(':', $db_host, 2);
			$db_host = $parts[0] ?? 'localhost';
			$this->_setDBPort((int) ($parts[1] ?? 3306));
		}
		$this->set('db_host', $db_host);
	}
	
	public function getDBHost() { return $this->get('db_host'); }

	private function _setDBPort($db_port) { $this->set('db_port', $db_port); }
	public function getDBPort() { return $this->get('db_port', Factory::getSettingsGeneral()->getMySQLDefaultPort()); }

	private function _setDBName($db_name) { $this->set('db_name', $db_name); }
	public function getDBName() { return $this->get('db_name'); }

	private function _setDBUser($db_user) { $this->set('db_user', $db_user); }
	public function getDBUser() { return $this->get('db_user'); }

	private function _setDBPassword($db_password) { $this->set('db_password', $db_password); }
	public function getDBPassword() { return $this->get('db_password'); }

	public function setInclude(array $include) {
		$this->setDumpSetting('include-tables', $include); 
		$this->setDumpSetting('include-views', $include); 
	}
	public function getInclude() { return $this->getDumpSetting('include-tables', []); }

	public function setExclude($exclude) { $this->setDumpSetting('no-data', $exclude); }
	public function getExclude() { return $this->getDumpSetting('no-data', []); }

	/**
	 * @param $buffer
	 *
	 * @return mixed|null
	 *
	 * Detect problematic SET commands that might involve '@OLD_' or '@saved_' variables
	 */
	private function _checkResume($buffer) {

		$problematicSetPattern = '/SET\s+\w+\s*=\s*@[\w_]+/i';

		if (preg_match($problematicSetPattern, $buffer)) {
			$this->getLogController()->logMessage("Skipping problematic statement: {$buffer}");
			return null; // Skip this query
		}

		return $buffer;
	}



	/**
	 * Override connect method with proper retry mechanism.
	 * @throws Exception
	 */
	protected function connect() {

		$attempts = 0;
		$waitTime = 500000; // Start with 500ms

		while ($attempts < self::QUERY_MAX_RETRIES) {
			try {
				$this->getLogController()->logDebug("Attempting to reconnect to MySQL (Attempt #$attempts)");

				// Call the parent connect method
				parent::connect();

				// If connection is successful, return
				if ($this->dbHandler) {
					$this->getLogController()->logDebug("Successfully reconnected to MySQL.");
					return;
				}

			} catch (Exception $e) {
				$this->getLogController()->logMessage("Reconnect attempt #$attempts failed (SQLSTATE: {$e->getCode()}): " . $e->getMessage());

				if (in_array($e->getCode(), self::SQLSTATE_ERRORS)) {
					$this->getLogController()->logMessage("Connection error detected (SQLSTATE: {$e->getCode()}), destroying dbHandler...");
					$this->dbHandler = null;
				}

				if ($attempts >= self::QUERY_MAX_RETRIES - 1) {
					throw new Exception("MySQL reconnect failed after $attempts attempts: " . $e->getMessage());
				}

				// Wait before retrying
				usleep($waitTime);
				$waitTime = min($waitTime * 2, 60000000); // Exponential backoff (max 60s)
			}

			$attempts++; // Increment attempt counter
		}
	}


	/**
	 * @throws Exception
	 */
	private static function AtomicWrite(string $path, string $content): bool
	{
		$_swap_file = $path . '.swap';

		try {

			// Write content to a temporary file
			if (file_put_contents($_swap_file, $content, LOCK_EX) === false) {
				$error = error_get_last();
				throw new Exception("Failed to write to temporary file: $_swap_file. Error: " . $error['message']);
			}


			// rename swap to target
			if (!@rename($_swap_file, $path)) {
				if (!file_exists($_swap_file) && file_exists($path)) return true;
				$error = error_get_last();
				throw new Exception("Failed to rename temporary file to target file: $path. Error: " . $error['message']);
			}

			return true;

		} catch (Exception $e) {
			throw new Exception($e->getMessage(), $e->getCode());
		}

	}

	/**
	 * @throws Exception
	 */

	public function import($path) {

		try {

			if (!$path || !is_file($path)) throw new Exception("[import] File {$path} does not exist.");

			$_table = basename($path);
			$_progress_file = $path . ".progress";
			$_progress_position = file_exists($_progress_file) ? (int)file_get_contents($_progress_file) : 0;

			$handle = fopen($path, 'rb');
			if (!$handle) throw new Exception("Failed reading file {$path}. Check access permissions.");

			if (!$this->dbHandler) $this->connect();

			// SQL mode 'NO_ENGINE_SUBSTITUTION' ensures that MySQL throws an error if the specified storage engine is unavailable,
			// preventing MySQL from automatically substituting it with the default engine, which helps maintain data integrity and consistency.
			$this->query_exec("SET sql_mode = 'NO_ENGINE_SUBSTITUTION';");

			// Seek to the last processed position if it exists
			if ($_progress_position > 0) {
				fseek($handle, $_progress_position);
				$this->getLogController()->logMessage("Resuming import for {$_table}, Position: {$_progress_position}");
			} else {
				$this->getLogController()->logMessage("Starting new import for: {$_table}");
			}

			$buffer = '';

			while (!feof($handle)) {

				$line = trim(fgets($handle));
				$currentPosition = ftell($handle);
				//$this->_log->write("Processing line at position: {$currentPosition}");
				if (substr($line, 0, 2) == '--' || !$line) continue; // skip comments
				$buffer .= $line;

				if (';' == substr(rtrim($line), -1, 1)) {
					try {
						$buffer = $this->_checkResume($buffer);
						// buffer is set to null at _checkResume function, to force skip set variables when resuming
						if ($buffer !== null) $this->query_exec($buffer);
						// Update the progress file with the current file position
						self::AtomicWrite($_progress_file, $currentPosition);
						if ($currentPosition % 1000 === 0) {
							$this->getLogController()->logMessage("Importing: $_table : $currentPosition ");
						}
						$buffer = '';

					} catch (PDOException $e) {

						$this->getLogController()->logMessage( "Failed to execute query: {$buffer}");
						$this->getLogController()->logMessage( "Error: " . $e->getMessage());
						$this->getLogController()->logMessage( "SQLSTATE: " . $e->getCode());

						throw new Exception( "Failed to execute query: {$buffer}");

					}
				}

				//sleep(1); // debug!
			}

			fclose($handle);
			$this->getLogController()->logMessage( "Finished importing {$_table}");

			// Remove the status file after the restore is complete
			if (is_file($_progress_file)) {
				@unlink($_progress_file);
				$this->getLogController()->logMessage( "Progress file for table removed");
			}

		} catch (Exception $e) {
			$this->getLogController()->logMessage( "Error: " . $e->getMessage());
			throw new Exception($e->getMessage());
		}
	}

	/**
	 * Execute a query that returns a result set (e.g., SELECT, SHOW TABLES).
	 *
	 * @param string $query The SQL query to execute.
	 *
	 * @return array|null Returns an array of results or null on failure.
	 * @throws Exception
	 */
	public function query_exec(string $query, array $params = []): ?array {

		$waitTime = 500000;
		$attempt = 0;
		$is_write_query = preg_match('/^\s*(INSERT|UPDATE|DELETE|REPLACE)/i', $query); // Detect write queries

		while ($attempt < self::QUERY_MAX_RETRIES) {
			try {
				if (!$this->dbHandler) {
					$this->getLogController()->logMessage("No database handler found, attempting to connect.");
					$this->connect();
				}

				// **Start transaction for write queries**
				if ($is_write_query) {
					if ($this->dbHandler->inTransaction()) {
						$this->getLogController()->logMessage("Warning: Already in a transaction, committing previous transaction before starting a new one.");
						$this->dbHandler->commit();
					}
					$this->getLogController()->logMessage("Starting transaction for: " . (strlen($query) > 50 ? substr($query, 0, 47) . "..." : $query));
					$this->dbHandler->beginTransaction();
					$this->dbHandler->setAttribute(PDO::ATTR_AUTOCOMMIT, 0);
				}

				$stmt = $this->dbHandler->prepare($query);
				$executionResult = $stmt->execute($params);

				if ($executionResult) {
					if ($is_write_query) {
						$this->getLogController()->logMessage("Committing transaction for: " . (strlen($query) > 50 ? substr($query, 0, 47) . "..." : $query));
						$this->dbHandler->commit();
						$this->dbHandler->setAttribute(PDO::ATTR_AUTOCOMMIT, 1);
					}
					return $stmt->fetchAll(PDO::FETCH_OBJ);
				} else {
					$this->getLogController()->logMessage("Query execution failed.");
					return null;
				}

			} catch (PDOException $e) {
				if ($is_write_query) {
					$this->getLogController()->logMessage("Rolling back transaction due to error.");
					$this->dbHandler->rollBack();
				}

				$sqlState = $e->getCode();
				if (in_array($sqlState, self::SQLSTATE_ERRORS)) {
					$this->getLogController()->logMessage("SQLSTATE $sqlState encountered, reconnecting...");
					$this->connect();
					usleep($waitTime);
					$waitTime = min($waitTime * 2, 60000000);
					$attempt++;
					continue;
				}
				throw new Exception("Query execution encountered an error: " . $e->getMessage(), $sqlState);
			}
		}
		return null;
	}
}