<?php defined('BASEPATH') or exit('No direct script access allowed');

class Auto_update extends Admin_Controller
{
	private function jobDir(): string
	{
		$dir = rtrim(get_temp_dir(), '/\\') . '/erp_update_jobs/';
		if (!is_dir($dir)) {
			@mkdir($dir, 0777, true);
		}
		return $dir;
	}

	private function jobFile(string $token): string
	{
		return $this->jobDir() . preg_replace('/[^a-zA-Z0-9_-]/', '', $token) . '.json';
	}

	private function lockFile(): string
	{
		return $this->jobDir() . 'update.lock';
	}

	private function lockAge(): int
	{
		$lockFile = $this->lockFile();
		if (!is_file($lockFile)) {
			return PHP_INT_MAX;
		}
		return time() - (int) @filemtime($lockFile);
	}

	private function staleAfter(): int
	{
		return 120;
	}

	private function readJob(string $token): array
	{
		$file = $this->jobFile($token);
		if (!is_file($file)) {
			return array();
		}
		$data = json_decode((string) file_get_contents($file), true);
		return is_array($data) ? $data : array();
	}

	private function writeJob(string $token, array $data): void
	{
		$data['token'] = $token;
		$data['updated_at'] = time();
		file_put_contents($this->jobFile($token), json_encode($data), LOCK_EX);
	}

	private function acquireLock(string $token): bool
	{
		$lockFile = $this->lockFile();
		if (is_file($lockFile)) {
			$current = trim((string) file_get_contents($lockFile));
			if ($current !== '' && $current !== $token && $this->lockAge() < 300) {
				return false;
			}
		}
		file_put_contents($lockFile, $token, LOCK_EX);
		return true;
	}

	private function releaseLock(string $token): void
	{
		$lockFile = $this->lockFile();
		if (is_file($lockFile)) {
			$current = trim((string) file_get_contents($lockFile));
			if ($current === $token) {
				@unlink($lockFile);
			}
		}
	}

	private function jsonResponse(array $payload): void
	{
		$this->output
			->set_content_type('application/json')
			->set_output(json_encode($payload));
	}

	private function upgradeAppConfig(): array
	{
		$this->load->helper('app_config');
		return app_config_upgrade();
	}

	private function canUseFunction(string $function): bool
	{
		if (!function_exists($function)) {
			return false;
		}

		$disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
		return !in_array($function, $disabled, true);
	}

	private function startWorker(string $token): void
	{
		if (!$this->canUseFunction('exec')) {
			$job = $this->readJob($token);
			$this->writeJob($token, array_merge($job, array(
				'status' => 'queued',
				'message' => 'Queued for browser-based processing',
				'done' => false,
				'error' => '',
			)));
			return;
		}

		$indexFile = FCPATH . 'index.php';
		$phpCandidates = array();

		if (defined('PHP_BINDIR')) {
			$phpCandidates[] = rtrim(PHP_BINDIR, '/\\') . DIRECTORY_SEPARATOR . 'php';
		}
		if (PHP_BINARY && stripos(PHP_BINARY, 'php-fpm') === false && stripos(PHP_BINARY, 'php-cgi') === false) {
			$phpCandidates[] = PHP_BINARY;
		}
		$phpCandidates[] = 'php';

		foreach (array_unique($phpCandidates) as $phpBinary) {
			if ($phpBinary !== 'php' && !is_executable($phpBinary)) {
				continue;
			}

			$command = escapeshellarg($phpBinary) . ' ' . escapeshellarg($indexFile) . ' admin auto_update_worker run ' . escapeshellarg($token) . ' > /dev/null 2>&1 &';
			@exec($command, $output, $exitCode);
			if ($exitCode === 0) {
				return;
			}
		}

		$workerUrl = site_url('admin/auto_update_worker/run/' . rawurlencode($token));
		$command = 'curl -k -s --max-time 10 ' . escapeshellarg($workerUrl) . ' > /dev/null 2>&1 &';
		@exec($command);
	}

	private function completeUpgradeIfReady(string $token, array $job, int $currentVersion, int $targetVersion): ?array
	{
		if ($currentVersion < $targetVersion) {
			return null;
		}

		$appConfig = $this->upgradeAppConfig();
		$payload = array(
			'status' => !empty($appConfig['success']) ? 'done' : 'failed',
			'progress' => 100,
			'current_version' => $currentVersion,
			'latest_version' => $targetVersion,
			'message' => !empty($appConfig['success']) ? 'Database upgrade completed successfully.' : $appConfig['message'],
			'done' => !empty($appConfig['success']),
			'error' => !empty($appConfig['success']) ? '' : $appConfig['message'],
		);
		$this->writeJob($token, array_merge($job, $payload));
		$this->releaseLock($token);

		return array_merge(array(
			'success' => !empty($appConfig['success']),
			'token' => $token,
		), $payload);
	}

	private function runMigrationStep(string $token, array $job): array
	{
		@set_time_limit(0);
		@ignore_user_abort(true);

		$this->load->config('migration');
		$this->load->library('migration', array(
			'migration_enabled' => true,
			'migration_type' => $this->config->item('migration_type'),
			'migration_table' => $this->config->item('migration_table'),
			'migration_auto_latest' => $this->config->item('migration_auto_latest'),
			'migration_version' => $this->config->item('migration_version'),
			'migration_path' => $this->config->item('migration_path')
		));

		$currentVersion = (int) $this->admin_model->get_current_db_version();
		$targetVersion = (int) ($job['latest_version'] ?? config_item('migration_version'));
		$baseVersion = (int) ($job['base_version'] ?? $currentVersion);
		$totalSteps = max(1, $targetVersion - $baseVersion);

		$finished = $this->completeUpgradeIfReady($token, $job, $currentVersion, $targetVersion);
		if ($finished !== null) {
			return $finished;
		}

		$nextVersion = $currentVersion + 1;
		$progress = max(1, (int) floor((($currentVersion - $baseVersion) / $totalSteps) * 100));
		$this->writeJob($token, array_merge($job, array(
			'status' => 'running',
			'message' => 'Applying migration ' . $nextVersion . ' of ' . $targetVersion,
			'progress' => $progress,
			'current_version' => $currentVersion,
			'latest_version' => $targetVersion,
			'done' => false,
			'error' => '',
		)));

		if ($this->migration->version($nextVersion) === FALSE) {
			$error = (string) $this->migration->error_string();
			$error = $error !== '' ? $error : 'Migration failed.';
			$this->writeJob($token, array_merge($job, array(
				'status' => 'failed',
				'message' => $error,
				'progress' => $progress,
				'current_version' => (int) $this->admin_model->get_current_db_version(),
				'latest_version' => $targetVersion,
				'done' => false,
				'error' => $error,
			)));
			$this->releaseLock($token);
			return array(
				'success' => false,
				'token' => $token,
				'status' => 'failed',
				'progress' => $progress,
				'current_version' => $currentVersion,
				'latest_version' => $targetVersion,
				'message' => $error,
				'done' => false,
				'error' => $error,
			);
		}

		$currentNow = (int) $this->admin_model->get_current_db_version();
		$progress = (int) floor((($currentNow - $baseVersion) / $totalSteps) * 100);
		if ($progress < 1) {
			$progress = 1;
		}

		$finished = $this->completeUpgradeIfReady($token, $job, $currentNow, $targetVersion);
		if ($finished !== null) {
			return $finished;
		}

		$this->writeJob($token, array_merge($job, array(
			'status' => 'running',
			'message' => 'Migration ' . $currentNow . ' completed',
			'progress' => $progress,
			'current_version' => $currentNow,
			'latest_version' => $targetVersion,
			'done' => false,
			'error' => '',
		)));

		return array(
			'success' => true,
			'token' => $token,
			'status' => 'running',
			'progress' => $progress,
			'current_version' => $currentNow,
			'latest_version' => $targetVersion,
			'message' => 'Migration ' . $currentNow . ' completed',
			'done' => false,
			'error' => '',
		);
	}

	public function index()
	{
		$this->output->set_output('');
	}

	public function database()
	{
		@set_time_limit(0);
		@ignore_user_abort(true);

		$this->load->config('migration');
		$currentVersion = (int) $this->admin_model->get_current_db_version();
		$targetVersion = (int) config_item('migration_version');

		if ($targetVersion <= 0) {
			$this->jsonResponse(array(
				'success' => false,
				'message' => 'Unable to detect the target database version.',
			));
			return;
		}

		if ($currentVersion >= $targetVersion) {
			$appConfig = $this->upgradeAppConfig();
			$this->jsonResponse(array(
				'success' => !empty($appConfig['success']),
				'done' => !empty($appConfig['success']),
				'progress' => 100,
				'current_version' => $currentVersion,
				'latest_version' => $targetVersion,
				'message' => !empty($appConfig['success']) ? 'Database is already up to date.' : $appConfig['message'],
				'error' => !empty($appConfig['success']) ? '' : $appConfig['message'],
			));
			return;
		}

		$lockFile = $this->lockFile();
		if (is_file($lockFile)) {
			$token = trim((string) file_get_contents($lockFile));
			$job = $token !== '' ? $this->readJob($token) : array();
			if (!empty($job)) {
				$age = time() - (int) ($job['updated_at'] ?? 0);
				if ($age < $this->staleAfter() && empty($job['done']) && ($job['status'] ?? '') !== 'failed') {
					$this->jsonResponse(array(
						'success' => true,
						'busy' => true,
						'token' => $token,
						'progress' => (int) ($job['progress'] ?? 0),
						'current_version' => (int) ($job['current_version'] ?? $currentVersion),
						'latest_version' => (int) ($job['latest_version'] ?? $targetVersion),
						'status' => (string) ($job['status'] ?? 'running'),
						'message' => (string) ($job['message'] ?? 'Update is already running.'),
					));
					return;
				}

				@unlink($lockFile);
				@unlink($this->jobFile($token));
			}
		}

		$token = bin2hex(random_bytes(8));
		file_put_contents($lockFile, $token, LOCK_EX);
		$this->writeJob($token, array(
			'status' => 'queued',
			'progress' => 0,
			'base_version' => $currentVersion,
			'current_version' => $currentVersion,
			'latest_version' => $targetVersion,
			'message' => 'Queued for processing',
			'done' => false,
			'error' => '',
		));
		$this->startWorker($token);

		$this->jsonResponse(array(
			'success' => true,
			'token' => $token,
			'busy' => false,
			'progress' => 0,
			'current_version' => $currentVersion,
			'latest_version' => $targetVersion,
			'message' => 'Background update started. Large database migrations may take several minutes.',
		));
	}

	public function status($token = '')
	{
		@set_time_limit(0);
		@ignore_user_abort(true);

		$token = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $token);
		if ($token === '') {
			$this->jsonResponse(array(
				'success' => false,
				'message' => 'Missing update token.',
			));
			return;
		}

		$job = $this->readJob($token);
		if (empty($job)) {
			$this->jsonResponse(array(
				'success' => false,
				'message' => 'Update job not found.',
			));
			return;
		}

		if (!empty($job['done']) || (string) ($job['status'] ?? '') === 'failed') {
			$this->jsonResponse(array(
				'success' => true,
				'token' => $token,
				'status' => (string) ($job['status'] ?? 'done'),
				'progress' => (int) ($job['progress'] ?? 100),
				'current_version' => (int) ($job['current_version'] ?? 0),
				'latest_version' => (int) ($job['latest_version'] ?? 0),
				'message' => (string) ($job['message'] ?? ''),
				'done' => !empty($job['done']),
				'error' => (string) ($job['error'] ?? ''),
			));
			return;
		}

		$currentVersion = (int) $this->admin_model->get_current_db_version();
		$targetVersion = (int) ($job['latest_version'] ?? config_item('migration_version'));
		$finished = $this->completeUpgradeIfReady($token, $job, $currentVersion, $targetVersion);
		if ($finished !== null) {
			$this->jsonResponse($finished);
			return;
		}

		$age = time() - (int) ($job['updated_at'] ?? 0);
		$message = (string) ($job['message'] ?? 'Update is running.');
		if ($age > 10) {
			try {
				$this->jsonResponse($this->runMigrationStep($token, $job));
				return;
			} catch (Throwable $e) {
				$error = $e->getMessage();
				$this->writeJob($token, array_merge($job, array(
					'status' => 'failed',
					'message' => 'Database upgrade failed.',
					'done' => false,
					'error' => $error,
				)));
				$this->releaseLock($token);
				$this->jsonResponse(array(
					'success' => false,
					'token' => $token,
					'status' => 'failed',
					'progress' => (int) ($job['progress'] ?? 0),
					'current_version' => $currentVersion,
					'latest_version' => $targetVersion,
					'message' => $error,
					'done' => false,
					'error' => $error,
				));
				return;
			}
		}

		if ($age > $this->staleAfter()) {
			$error = 'No database upgrade progress was reported for more than 2 minutes. Please retry the upgrade.';
			$this->writeJob($token, array_merge($job, array(
				'status' => 'failed',
				'message' => $error,
				'done' => false,
				'error' => $error,
			)));
			$this->releaseLock($token);
			$this->jsonResponse(array(
				'success' => false,
				'token' => $token,
				'status' => 'failed',
				'progress' => (int) ($job['progress'] ?? 0),
				'current_version' => (int) ($job['current_version'] ?? 0),
				'latest_version' => (int) ($job['latest_version'] ?? 0),
				'message' => $error,
				'done' => false,
				'error' => $error,
			));
			return;
		}

		$this->jsonResponse(array(
			'success' => true,
			'token' => $token,
			'status' => (string) ($job['status'] ?? 'running'),
			'progress' => (int) ($job['progress'] ?? 0),
			'current_version' => (int) ($job['current_version'] ?? 0),
			'latest_version' => (int) ($job['latest_version'] ?? 0),
			'message' => $message,
			'done' => false,
			'error' => '',
		));
	}

	public function unlock($token = '')
	{
		$token = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $token);
		$this->releaseLock($token);
	}
}
