<?php
/* Copyright (C) 2026  Eduard Wisch  <data@data-it-solution.de>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 */

/**
 * \file    bankimport/class/bankimportcron.class.php
 * \ingroup bankimport
 * \brief   Cron job class for automatic bank statement import
 */

require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
dol_include_once('/bankimport/class/fints.class.php');
dol_include_once('/bankimport/class/banktransaction.class.php');
dol_include_once('/bankimport/class/bankstatement.class.php');

/**
 * Class BankImportCron
 * Handles automatic bank statement import via scheduled task
 */
class BankImportCron
{
	/**
	 * @var DoliDB Database handler
	 */
	public $db;

	/**
	 * @var string Error message
	 */
	public $error = '';

	/**
	 * @var array Error messages
	 */
	public $errors = array();

	/**
	 * @var string Output message for cron log
	 */
	public $output = '';

	/**
	 * @var string Path to cron log file
	 */
	private $cronLogFile = '';

	/**
	 * @var float Start time of cron execution
	 */
	private $startTime = 0;

	/**
	 * Constructor
	 *
	 * @param DoliDB $db Database handler
	 */
	public function __construct($db)
	{
		global $conf;
		$this->db = $db;

		// Set up dedicated log file for cron jobs
		$logDir = $conf->bankimport->dir_output.'/logs';
		if (!is_dir($logDir)) {
			dol_mkdir($logDir);
		}
		$this->cronLogFile = $logDir.'/cron_bankimport.log';
	}

	/**
	 * Write to dedicated cron log file
	 *
	 * @param string $message Log message
	 * @param string $level   Log level (INFO, WARNING, ERROR, DEBUG)
	 * @return void
	 */
	private function cronLog($message, $level = 'INFO')
	{
		$timestamp = date('Y-m-d H:i:s');
		$elapsed = $this->startTime > 0 ? round(microtime(true) - $this->startTime, 2).'s' : '0s';
		$logLine = "[{$timestamp}] [{$level}] [{$elapsed}] {$message}\n";

		// Write to dedicated log file
		@file_put_contents($this->cronLogFile, $logLine, FILE_APPEND | LOCK_EX);

		// Also log to Dolibarr system log
		$dolLevel = LOG_INFO;
		if ($level === 'ERROR') $dolLevel = LOG_ERR;
		elseif ($level === 'WARNING') $dolLevel = LOG_WARNING;
		elseif ($level === 'DEBUG') $dolLevel = LOG_DEBUG;

		dol_syslog("BankImportCron: ".$message, $dolLevel);
	}

	/**
	 * Record cron execution status to database for monitoring
	 *
	 * @param string $status  Status (started, running, completed, error)
	 * @param string $message Status message
	 * @return void
	 */
	private function recordCronStatus($status, $message = '')
	{
		global $conf;

		$statusData = array(
			'status' => $status,
			'message' => $message,
			'timestamp' => time(),
			'duration' => $this->startTime > 0 ? round(microtime(true) - $this->startTime, 2) : 0,
			'memory' => memory_get_usage(true),
			'peak_memory' => memory_get_peak_usage(true)
		);

		dolibarr_set_const($this->db, 'BANKIMPORT_CRON_STATUS', json_encode($statusData), 'chaine', 0, '', $conf->entity);
	}

	/**
	 * Execute the automatic import cron job
	 * Called by Dolibarr's scheduled task system
	 *
	 * @return int 0 if OK, < 0 if error
	 */
	public function doAutoImport()
	{
		global $conf, $langs, $user;

		// Initialize timing
		$this->startTime = microtime(true);

		// Register shutdown function to catch fatal errors
		register_shutdown_function(array($this, 'handleShutdown'));

		$langs->load('bankimport@bankimport');

		$this->cronLog("========== CRON START ==========");
		$this->cronLog("PHP Version: ".PHP_VERSION.", Memory Limit: ".ini_get('memory_limit').", Max Execution Time: ".ini_get('max_execution_time'));
		$this->recordCronStatus('started', 'Cron job started');

		// Check if automatic import is enabled
		if (!getDolGlobalInt('BANKIMPORT_AUTO_ENABLED')) {
			$this->output = $langs->trans('AutoImportDisabled');
			$this->cronLog("Auto import is disabled - exiting");
			$this->recordCronStatus('completed', 'Auto import disabled');
			return 0;
		}

		// CHECK: Is cron paused due to errors? (to prevent bank account lockout)
		$pausedUntil = getDolGlobalInt('BANKIMPORT_CRON_PAUSED_UNTIL');
		if ($pausedUntil > 0 && $pausedUntil > time()) {
			$pauseReason = getDolGlobalString('BANKIMPORT_CRON_PAUSE_REASON');
			$remainingMinutes = ceil(($pausedUntil - time()) / 60);
			$this->output = "Cron pausiert für {$remainingMinutes} Minuten: {$pauseReason}";
			$this->cronLog("Cron is PAUSED until ".date('Y-m-d H:i:s', $pausedUntil)." - Reason: {$pauseReason}", 'WARNING');
			$this->recordCronStatus('paused', "Paused: {$pauseReason}");
			return 0;
		}

		// Clear pause if expired
		if ($pausedUntil > 0 && $pausedUntil <= time()) {
			$this->cronLog("Pause expired - resuming normal operation");
			dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PAUSED_UNTIL', $conf->entity);
			dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PAUSE_REASON', $conf->entity);
			dolibarr_del_const($this->db, 'BANKIMPORT_CRON_FAIL_COUNT', $conf->entity);
		}

		// Check consecutive failure count
		$failCount = getDolGlobalInt('BANKIMPORT_CRON_FAIL_COUNT');
		if ($failCount >= 3) {
			// After 3 consecutive failures, pause for increasing time
			$pauseMinutes = min(60 * 24, 15 * pow(2, $failCount - 3)); // 15min, 30min, 1h, 2h, ... max 24h
			$this->pauseCron("Zu viele Fehlversuche ({$failCount}x) - Bitte manuell prüfen", $pauseMinutes);
			$this->output = "Automatisch pausiert nach {$failCount} Fehlversuchen";
			$this->cronLog("Auto-paused after {$failCount} failures for {$pauseMinutes} minutes", 'WARNING');
			return 0;
		}

		$this->cronLog("Initializing FinTS library");
		$this->recordCronStatus('running', 'Initializing FinTS');

		// Initialize FinTS
		$fints = new BankImportFinTS($this->db);

		if (!$fints->isConfigured()) {
			$this->error = $langs->trans('AutoImportNotConfigured');
			$this->cronLog("FinTS not configured", 'WARNING');
			$this->setNotification('config_error');
			$this->recordCronStatus('error', 'FinTS not configured');
			return -1;
		}

		if (!$fints->isLibraryAvailable()) {
			$this->error = $langs->trans('FinTSLibraryNotFound');
			$this->cronLog("FinTS library not found", 'ERROR');
			$this->recordCronStatus('error', 'FinTS library not found');
			return -1;
		}

		$this->cronLog("FinTS library loaded successfully");

		// Check for stored session state (from previous successful TAN)
		$storedState = getDolGlobalString('BANKIMPORT_CRON_STATE');
		$sessionRestored = false;

		try {
			// Try to restore previous session
			if (!empty($storedState)) {
				$this->cronLog("Attempting to restore previous session (state length: ".strlen($storedState).")");
				$this->recordCronStatus('running', 'Restoring session');
				$restoreResult = $fints->restore($storedState);
				if ($restoreResult < 0) {
					// Session expired, need fresh login
					$this->cronLog("Session restore failed: ".$fints->error.", trying fresh login", 'WARNING');
					$storedState = '';
					// Clear stale session
					dolibarr_del_const($this->db, 'BANKIMPORT_CRON_STATE', $conf->entity);
				} else {
					$this->cronLog("Session restored successfully");
					$sessionRestored = true;
				}
			} else {
				$this->cronLog("No stored session found");
			}

			// If no stored session or restore failed, try fresh login
			if (empty($storedState)) {
				$this->cronLog("Attempting fresh login to bank");
				$this->recordCronStatus('running', 'Logging in to bank');
				$loginResult = $fints->login();

				if ($loginResult < 0) {
					$this->error = $langs->trans('LoginFailed').': '.$fints->error;
					$this->cronLog("Login failed: ".$fints->error, 'ERROR');
					$this->setNotification('login_error');
					$this->recordCronStatus('error', 'Login failed: '.$fints->error);

					// Increment failure count and potentially pause to prevent lockout
					$this->incrementFailCount();

					// Check if this is a critical auth error that should pause immediately
					if ($this->isAuthError($fints->error)) {
						$this->pauseCron("Bank-Login fehlgeschlagen - Zugangsdaten prüfen!", 60); // Pause 1 hour
						$this->cronLog("CRITICAL: Auth error detected - pausing cron for 1 hour to prevent lockout", 'ERROR');
					}

					return -1;
				}

				if ($loginResult == 0) {
					// TAN required - can't proceed automatically
					$this->output = $langs->trans('TANRequired');
					$this->cronLog("TAN required for login - cannot proceed automatically", 'WARNING');
					$this->setNotification('tan_required');
					$this->recordCronStatus('completed', 'TAN required - waiting for user');

					// Store the state so user can complete TAN manually
					$state = $fints->persist();
					if (!empty($state)) {
						dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $state, 'chaine', 0, '', $conf->entity);
						dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', serialize($fints->getPendingAction()), 'chaine', 0, '', $conf->entity);
					}
					$this->cronLog("========== CRON END (TAN required) ==========");
					return 0; // Not an error, just can't proceed
				}

				$this->cronLog("Fresh login successful");
			}

			// Login successful or session restored - fetch statements
			$daysToFetch = getDolGlobalInt('BANKIMPORT_AUTO_DAYS') ?: 30;
			$dateFrom = strtotime("-{$daysToFetch} days");
			$dateTo = time();

			$this->cronLog("Fetching statements from ".date('Y-m-d', $dateFrom)." to ".date('Y-m-d', $dateTo)." ({$daysToFetch} days)");
			$this->recordCronStatus('running', 'Fetching bank statements');

			$result = $fints->fetchStatements($dateFrom, $dateTo);

			// Log what we got back
			if (is_array($result)) {
				$txCount = count($result['transactions'] ?? array());
				$hasBalance = !empty($result['balance']);
				$isPartial = !empty($result['partial']);
				$this->cronLog("fetchStatements returned: transactions={$txCount}, hasBalance=".($hasBalance?'yes':'no').", partial=".($isPartial?'yes':'no'));
			} else {
				$this->cronLog("fetchStatements returned: ".var_export($result, true));
			}

			if ($result === 0) {
				// TAN required for statements
				$this->output = $langs->trans('TANRequired');
				$this->cronLog("TAN required for statements", 'WARNING');
				$this->setNotification('tan_required');
				$this->recordCronStatus('completed', 'TAN required for statements');

				// Store state for manual completion
				$state = $fints->persist();
				if (!empty($state)) {
					dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $state, 'chaine', 0, '', $conf->entity);
					dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', serialize($fints->getPendingAction()), 'chaine', 0, '', $conf->entity);
				}
				$this->cronLog("========== CRON END (TAN required) ==========");
				return 0;
			}

			if ($result < 0) {
				$this->error = $langs->trans('FetchFailed').': '.$fints->error;
				$this->cronLog("Fetch failed: ".$fints->error, 'ERROR');
				$this->setNotification('fetch_error');
				$this->recordCronStatus('error', 'Fetch failed: '.$fints->error);
				return -1;
			}

			// Success - import transactions
			$transactions = $result['transactions'] ?? array();
			$fetchedCount = count($transactions);
			$importedCount = 0;
			$skippedCount = 0;

			$this->cronLog("Bank returned {$fetchedCount} transactions");

			// If restored session returned 0 transactions, the session might be stale
			// Try fresh login as fallback
			if ($fetchedCount == 0 && $sessionRestored) {
				$this->cronLog("Restored session returned 0 transactions - session might be stale", 'WARNING');
				dolibarr_del_const($this->db, 'BANKIMPORT_CRON_STATE', $conf->entity);

				// Note: We don't retry here because a fresh login would require TAN
				// Just mark that the session was stale
				$this->output = $langs->trans('AutoImportNoTransactions').' (Session abgelaufen - nächster Lauf erfordert TAN)';

				$this->setNotification('session_expired');
				$this->recordCronStatus('completed', 'Session expired - no transactions');
				$this->cronLog("========== CRON END (session expired) ==========");
				return 0;
			}

			if (!empty($transactions)) {
				$this->cronLog("Starting import of {$fetchedCount} transactions");
				$this->recordCronStatus('running', "Importing {$fetchedCount} transactions");

				// Get a system user for the import
				$importUser = new User($this->db);
				$importUser->fetch(1); // Admin user

				$iban = $fints->getIban();

				// Use the importFromFinTS method for correct field mapping
				$transImporter = new BankImportTransaction($this->db);
				$importResult = $transImporter->importFromFinTS($transactions, $iban, $importUser);

				$importedCount = $importResult['imported'] ?? 0;
				$skippedCount = $importResult['skipped'] ?? 0;

				$this->cronLog("Import result: imported={$importedCount}, skipped={$skippedCount}");
			}

			// Update last fetch info
			dolibarr_set_const($this->db, 'BANKIMPORT_LAST_FETCH', time(), 'chaine', 0, '', $conf->entity);
			dolibarr_set_const($this->db, 'BANKIMPORT_LAST_FETCH_COUNT', $importedCount, 'chaine', 0, '', $conf->entity);

			// Store session for next run (might avoid TAN)
			$state = $fints->persist();
			if (!empty($state)) {
				dolibarr_set_const($this->db, 'BANKIMPORT_CRON_STATE', $state, 'chaine', 0, '', $conf->entity);
				$this->cronLog("Session state saved for next run");
			}

			// Clear any pending state
			dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity);
			dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity);

			// Clear notification flag on success
			$this->clearNotification();

			$fints->close();

			if ($importedCount > 0) {
				$this->output = $langs->trans('AutoImportSuccess', $importedCount);
			} elseif ($skippedCount > 0) {
				$this->output = $langs->trans('AutoImportNoTransactions').' ('.$skippedCount.' bereits vorhanden)';
			} else {
				$this->output = $langs->trans('AutoImportNoTransactions');
			}

			// Reset failure count on success
			$this->resetFailCount();

			$duration = round(microtime(true) - $this->startTime, 2);
			$this->cronLog("Completed successfully: imported={$importedCount}, skipped={$skippedCount}, duration={$duration}s");
			$this->recordCronStatus('completed', "Success: imported={$importedCount}, skipped={$skippedCount}");
			$this->cronLog("========== CRON END (success) ==========");

			return 0;

		} catch (Exception $e) {
			$this->error = 'Exception: '.$e->getMessage();
			$this->cronLog("EXCEPTION: ".$e->getMessage()."\nStack trace:\n".$e->getTraceAsString(), 'ERROR');
			$this->setNotification('error');
			$this->recordCronStatus('error', 'Exception: '.$e->getMessage());
			$this->incrementFailCount();
			$this->cronLog("========== CRON END (exception) ==========");
			return -1;
		} catch (Throwable $t) {
			$this->error = 'Fatal error: '.$t->getMessage();
			$this->cronLog("FATAL ERROR: ".$t->getMessage()."\nStack trace:\n".$t->getTraceAsString(), 'ERROR');
			$this->setNotification('error');
			$this->recordCronStatus('error', 'Fatal: '.$t->getMessage());
			$this->incrementFailCount();
			$this->cronLog("========== CRON END (fatal error) ==========");
			return -1;
		}
	}

	/**
	 * Pause cron execution for specified minutes
	 *
	 * @param string $reason Reason for pausing
	 * @param int    $minutes Minutes to pause
	 * @return void
	 */
	private function pauseCron($reason, $minutes = 60)
	{
		global $conf;

		$pauseUntil = time() + ($minutes * 60);
		dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PAUSED_UNTIL', $pauseUntil, 'chaine', 0, '', $conf->entity);
		dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PAUSE_REASON', $reason, 'chaine', 0, '', $conf->entity);

		$this->cronLog("CRON PAUSED until ".date('Y-m-d H:i:s', $pauseUntil)." - Reason: {$reason}", 'WARNING');
		$this->setNotification('paused');
	}

	/**
	 * Increment consecutive failure count
	 *
	 * @return int New failure count
	 */
	private function incrementFailCount()
	{
		global $conf;

		$count = getDolGlobalInt('BANKIMPORT_CRON_FAIL_COUNT') + 1;
		dolibarr_set_const($this->db, 'BANKIMPORT_CRON_FAIL_COUNT', $count, 'chaine', 0, '', $conf->entity);
		$this->cronLog("Failure count incremented to {$count}");
		return $count;
	}

	/**
	 * Reset failure count (on success)
	 *
	 * @return void
	 */
	private function resetFailCount()
	{
		global $conf;

		$oldCount = getDolGlobalInt('BANKIMPORT_CRON_FAIL_COUNT');
		if ($oldCount > 0) {
			dolibarr_del_const($this->db, 'BANKIMPORT_CRON_FAIL_COUNT', $conf->entity);
			$this->cronLog("Failure count reset (was {$oldCount})");
		}
	}

	/**
	 * Check if error message indicates an authentication error
	 * These errors should pause immediately to prevent account lockout
	 *
	 * @param string $error Error message
	 * @return bool True if this is an auth-related error
	 */
	private function isAuthError($error)
	{
		$authKeywords = array(
			'authentication',
			'authentifizierung',
			'passwort',
			'password',
			'pin',
			'credentials',
			'zugangsdaten',
			'gesperrt',
			'locked',
			'blocked',
			'invalid user',
			'ungültiger benutzer',
			'zugang verweigert',
			'access denied',
			'not authorized',
			'nicht autorisiert'
		);

		$errorLower = strtolower($error);
		foreach ($authKeywords as $keyword) {
			if (strpos($errorLower, $keyword) !== false) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Manually unpause the cron (called from admin interface)
	 *
	 * @return bool Success
	 */
	public function unpauseCron()
	{
		global $conf;

		dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PAUSED_UNTIL', $conf->entity);
		dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PAUSE_REASON', $conf->entity);
		dolibarr_del_const($this->db, 'BANKIMPORT_CRON_FAIL_COUNT', $conf->entity);
		$this->clearNotification();

		$this->cronLog("Cron manually unpaused by admin");
		return true;
	}

	/**
	 * Get current cron status for display
	 *
	 * @return array Status information
	 */
	public static function getCronStatus()
	{
		global $conf;

		$status = array(
			'enabled' => getDolGlobalInt('BANKIMPORT_AUTO_ENABLED') > 0,
			'paused' => false,
			'paused_until' => 0,
			'pause_reason' => '',
			'fail_count' => getDolGlobalInt('BANKIMPORT_CRON_FAIL_COUNT'),
			'last_run' => null,
			'notification' => null
		);

		// Check if paused
		$pausedUntil = getDolGlobalInt('BANKIMPORT_CRON_PAUSED_UNTIL');
		if ($pausedUntil > 0 && $pausedUntil > time()) {
			$status['paused'] = true;
			$status['paused_until'] = $pausedUntil;
			$status['pause_reason'] = getDolGlobalString('BANKIMPORT_CRON_PAUSE_REASON');
		}

		// Get last run status
		$lastStatus = getDolGlobalString('BANKIMPORT_CRON_STATUS');
		if (!empty($lastStatus)) {
			$status['last_run'] = json_decode($lastStatus, true);
		}

		// Get notification
		$status['notification'] = self::getNotification();

		return $status;
	}

	/**
	 * Shutdown handler to catch fatal errors
	 * Called automatically by PHP when script ends
	 *
	 * @return void
	 */
	public function handleShutdown()
	{
		$error = error_get_last();
		if ($error !== null && in_array($error['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) {
			$message = "FATAL SHUTDOWN: {$error['message']} in {$error['file']}:{$error['line']}";
			$this->cronLog($message, 'ERROR');
			$this->recordCronStatus('error', $message);
			$this->cronLog("========== CRON END (fatal shutdown) ==========");
		}
	}

	/**
	 * Set notification flag for admin users
	 * Also sends to GlobalNotify if available
	 *
	 * @param string $type Notification type (tan_required, error, etc.)
	 * @return void
	 */
	private function setNotification($type)
	{
		global $conf;

		dolibarr_set_const($this->db, 'BANKIMPORT_NOTIFICATION', $type, 'chaine', 0, '', $conf->entity);
		dolibarr_set_const($this->db, 'BANKIMPORT_NOTIFICATION_DATE', time(), 'chaine', 0, '', $conf->entity);

		// Send to GlobalNotify if module is enabled
		if (isModEnabled('globalnotify')) {
			dol_include_once('/globalnotify/class/globalnotify.class.php');
			if (class_exists('GlobalNotify')) {
				$messages = array(
					'tan_required' => array('TAN erforderlich', 'Bank-Login erfordert TAN-Bestätigung', 'action'),
					'login_error' => array('Login-Fehler', 'Bank-Login fehlgeschlagen - Zugangsdaten prüfen', 'error'),
					'fetch_error' => array('Abruf-Fehler', 'Kontoauszüge konnten nicht abgerufen werden', 'error'),
					'config_error' => array('Konfigurationsfehler', 'FinTS ist nicht korrekt konfiguriert', 'error'),
					'session_expired' => array('Session abgelaufen', 'Bank-Session ist abgelaufen, neuer Login erforderlich', 'warning'),
					'paused' => array('Cron pausiert', 'BankImport Cron wurde automatisch pausiert', 'warning'),
					'error' => array('Allgemeiner Fehler', 'Ein Fehler ist aufgetreten', 'error'),
				);

				if (isset($messages[$type])) {
					$msg = $messages[$type];
					$actionUrl = dol_buildpath('/bankimport/admin/cronmonitor.php', 1);
					$actionLabel = 'Details anzeigen';

					if ($type == 'tan_required') {
						$actionUrl = dol_buildpath('/bankimport/fetch.php', 1);
						$actionLabel = 'TAN eingeben';
					}

					$notify = new GlobalNotify($this->db);
					$notifyType = $msg[2] == 'action' ? GlobalNotify::TYPE_ACTION :
								 ($msg[2] == 'error' ? GlobalNotify::TYPE_ERROR : GlobalNotify::TYPE_WARNING);

					$notify->addNotification(
						'bankimport',
						$notifyType,
						$msg[0],
						$msg[1],
						$actionUrl,
						$actionLabel,
						$msg[2] == 'error' ? 10 : ($msg[2] == 'action' ? 9 : 7)
					);
				}
			}
		}
	}

	/**
	 * Clear notification flag
	 *
	 * @return void
	 */
	private function clearNotification()
	{
		global $conf;

		dolibarr_del_const($this->db, 'BANKIMPORT_NOTIFICATION', $conf->entity);
		dolibarr_del_const($this->db, 'BANKIMPORT_NOTIFICATION_DATE', $conf->entity);
	}

	/**
	 * Check if there's a pending notification
	 *
	 * @return array|null Notification info or null
	 */
	public static function getNotification()
	{
		$type = getDolGlobalString('BANKIMPORT_NOTIFICATION');
		$date = getDolGlobalInt('BANKIMPORT_NOTIFICATION_DATE');

		if (empty($type)) {
			return null;
		}

		return array(
			'type' => $type,
			'date' => $date
		);
	}

	/**
	 * Resume a pending TAN action (called from web interface)
	 *
	 * @return int 1 if TAN confirmed and import done, 0 if still waiting, -1 if error
	 */
	public function resumePendingAction()
	{
		global $conf, $langs, $user;

		$pendingState = getDolGlobalString('BANKIMPORT_CRON_PENDING_STATE');
		$pendingAction = getDolGlobalString('BANKIMPORT_CRON_PENDING_ACTION');

		if (empty($pendingState) || empty($pendingAction)) {
			$this->error = 'No pending action';
			return -1;
		}

		$fints = new BankImportFinTS($this->db);

		$restoreResult = $fints->restore($pendingState);
		if ($restoreResult < 0) {
			$this->error = $fints->error;
			// Clear expired state
			dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity);
			dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity);
			return -1;
		}

		$action = @unserialize($pendingAction);
		if ($action === false) {
			$this->error = 'Could not restore pending action';
			return -1;
		}

		$fints->setPendingAction($action);

		// Check if TAN was confirmed
		$checkResult = $fints->checkDecoupledTan();

		if ($checkResult == 0) {
			// Still waiting
			// Update state
			$newState = $fints->persist();
			if (!empty($newState)) {
				dolibarr_set_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $newState, 'chaine', 0, '', $conf->entity);
			}
			return 0;
		}

		if ($checkResult < 0) {
			$this->error = $fints->error;
			return -1;
		}

		// TAN confirmed - now run the import
		dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_STATE', $conf->entity);
		dolibarr_del_const($this->db, 'BANKIMPORT_CRON_PENDING_ACTION', $conf->entity);

		// Store the confirmed session and run import
		$state = $fints->persist();
		if (!empty($state)) {
			dolibarr_set_const($this->db, 'BANKIMPORT_CRON_STATE', $state, 'chaine', 0, '', $conf->entity);
		}

		return $this->doAutoImport();
	}

	/**
	 * Execute automatic PDF statement fetch via HKEKP
	 * Called by Dolibarr's scheduled task system
	 *
	 * @return int 0 if OK, < 0 if error
	 */
	public function doAutoFetchPdf()
	{
		global $conf, $langs, $user;

		// Initialize timing
		$this->startTime = microtime(true);

		$langs->load('bankimport@bankimport');

		$this->cronLog("========== PDF FETCH CRON START ==========");
		$this->recordCronStatus('started', 'PDF fetch cron started');

		// Check if PDF fetch is enabled
		if (!getDolGlobalInt('BANKIMPORT_PDF_AUTO_ENABLED')) {
			$this->output = $langs->trans('AutoPdfFetchDisabled');
			$this->cronLog("Auto PDF fetch is disabled - exiting");
			$this->recordCronStatus('completed', 'Auto PDF fetch disabled');
			return 0;
		}

		// Check cron pause status (shared with main cron)
		$pausedUntil = getDolGlobalInt('BANKIMPORT_CRON_PAUSED_UNTIL');
		if ($pausedUntil > 0 && $pausedUntil > time()) {
			$pauseReason = getDolGlobalString('BANKIMPORT_CRON_PAUSE_REASON');
			$this->output = "Cron pausiert: {$pauseReason}";
			$this->cronLog("Cron is PAUSED - skipping PDF fetch", 'WARNING');
			return 0;
		}

		$this->cronLog("Initializing FinTS for PDF fetch");

		// Initialize FinTS
		$fints = new BankImportFinTS($this->db);

		if (!$fints->isConfigured()) {
			$this->error = $langs->trans('AutoImportNotConfigured');
			$this->cronLog("FinTS not configured", 'WARNING');
			return -1;
		}

		try {
			// Login to bank
			$this->cronLog("Logging in to bank");
			$loginResult = $fints->login();

			if ($loginResult < 0) {
				$this->error = $fints->error;
				$this->cronLog("Login failed: ".$fints->error, 'ERROR');
				return -1;
			}

			if ($loginResult == 0) {
				// TAN required
				$this->output = $langs->trans('TANRequired');
				$this->cronLog("TAN required for PDF fetch", 'WARNING');
				$this->recordCronStatus('completed', 'TAN required');
				return 0;
			}

			// Check if bank supports PDF statements
			if (!$fints->supportsPdfStatements()) {
				$this->output = $langs->trans('ErrorBankDoesNotSupportPdfStatements');
				$this->cronLog("Bank does not support PDF statements (HKEKP)", 'WARNING');
				$fints->close();
				return 0;
			}

			// Fetch PDF statement
			$this->cronLog("Fetching PDF statement via HKEKP");
			$pdfResult = $fints->getStatementPDF(0);

			if ($pdfResult === 0) {
				// TAN required
				$this->output = $langs->trans('TANRequired');
				$this->cronLog("TAN required for PDF statement", 'WARNING');
				$fints->close();
				return 0;
			}

			if ($pdfResult === -1) {
				$this->error = $fints->error;
				$this->cronLog("PDF fetch failed: ".$fints->error, 'ERROR');
				$fints->close();
				return -1;
			}

			// Check if we got any data
			if (empty($pdfResult['pdfData'])) {
				$this->output = $langs->trans('NoPdfStatementsAvailable');
				$this->cronLog("No new PDF statements available");
				$fints->close();
				return 0;
			}

			// Save the PDF
			$info = $pdfResult['info'];
			$pdfData = $pdfResult['pdfData'];

			$this->cronLog("Received PDF statement #".$info['statementNumber'].'/'.$info['statementYear']);

			// Check if statement already exists
			$stmt = new BankImportStatement($this->db);
			$stmt->statement_number = $info['statementNumber'];
			$stmt->statement_year = $info['statementYear'];
			$stmt->iban = $info['iban'] ?: getDolGlobalString('BANKIMPORT_IBAN');

			if ($stmt->exists()) {
				$this->output = $langs->trans("StatementAlreadyExists").': '.$stmt->statement_number.'/'.$stmt->statement_year;
				$this->cronLog("Statement already exists - skipping");
				$fints->close();
				return 0;
			}

			// Save PDF to file
			$dir = BankImportStatement::getStorageDir();
			$ibanPart = preg_replace('/[^A-Z0-9]/', '', strtoupper($stmt->iban ?: 'KONTO'));
			$filename = sprintf('Kontoauszug_%s_%d_%s.pdf',
				$ibanPart,
				$stmt->statement_year,
				str_pad($stmt->statement_number, 3, '0', STR_PAD_LEFT)
			);

			$filepath = $dir.'/'.$filename;
			if (file_put_contents($filepath, $pdfData) === false) {
				$this->error = $langs->trans("ErrorSavingPdfFile");
				$this->cronLog("Failed to save PDF file", 'ERROR');
				$fints->close();
				return -1;
			}

			// Create database record
			$stmt->filename = $filename;
			$stmt->filepath = $filepath;
			$stmt->filesize = strlen($pdfData);
			$stmt->statement_date = $info['creationDate'] ? $info['creationDate']->getTimestamp() : dol_now();
			$stmt->import_key = 'fints_cron_'.date('YmdHis');

			// Get system user
			$importUser = new User($this->db);
			$importUser->fetch(1);

			$result = $stmt->create($importUser);

			if ($result > 0) {
				$this->output = $langs->trans("PdfStatementFetched", $stmt->statement_number.'/'.$stmt->statement_year);
				$this->cronLog("PDF statement saved successfully: ".$stmt->statement_number.'/'.$stmt->statement_year);

				// Update last fetch timestamp
				dolibarr_set_const($this->db, 'BANKIMPORT_PDF_LAST_FETCH', time(), 'chaine', 0, '', $conf->entity);
			} else {
				$this->error = $stmt->error;
				$this->cronLog("Failed to create database record: ".$stmt->error, 'ERROR');
				@unlink($filepath);
				$fints->close();
				return -1;
			}

			$fints->close();

			$duration = round(microtime(true) - $this->startTime, 2);
			$this->cronLog("PDF fetch completed successfully in {$duration}s");
			$this->recordCronStatus('completed', "PDF fetched: {$stmt->statement_number}/{$stmt->statement_year}");
			$this->cronLog("========== PDF FETCH CRON END ==========");

			return 0;

		} catch (Exception $e) {
			$this->error = 'Exception: '.$e->getMessage();
			$this->cronLog("EXCEPTION: ".$e->getMessage(), 'ERROR');
			$this->recordCronStatus('error', 'Exception: '.$e->getMessage());
			return -1;
		}
	}
}
