<?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/fints.class.php
 * \ingroup bankimport
 * \brief   Class for FinTS/HBCI bank connection
 */

// Load Dolibarr's PSR classes first to avoid version conflicts
// Dolibarr includes psr/log 1.x which must be loaded before phpFinTS
if (defined('DOL_DOCUMENT_ROOT')) {
	$dolibarrPsrLog = DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/LoggerInterface.php';
	if (file_exists($dolibarrPsrLog) && !interface_exists('Psr\Log\LoggerInterface', false)) {
		require_once DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/LoggerInterface.php';
		require_once DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/LoggerTrait.php';
		require_once DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/AbstractLogger.php';
		require_once DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/NullLogger.php';
		require_once DOL_DOCUMENT_ROOT.'/includes/sabre/psr/log/Psr/Log/LogLevel.php';
	}
}

// Autoload phpFinTS library if available
$composerAutoload = dirname(__DIR__).'/vendor/autoload.php';
if (file_exists($composerAutoload)) {
	require_once $composerAutoload;
}

use Fhp\FinTs;
use Fhp\Options\FinTsOptions;
use Fhp\Options\Credentials;
use Fhp\Action\GetSEPAAccounts;
use Fhp\Action\GetStatementOfAccount;
use Fhp\Action\GetStatementOfAccountXML;
use Fhp\Action\GetStatementPDF;
use Fhp\Action\GetStatementFromArchive;
use Fhp\Model\StatementOfAccount\Statement;
use Fhp\Model\StatementOfAccount\Transaction;

/**
 * Class BankImportFinTS
 * Handles FinTS/HBCI bank connections for retrieving account statements
 */
class BankImportFinTS
{
	/**
	 * @var DoliDB Database handler
	 */
	public $db;

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

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

	/**
	 * @var string FinTS Server URL
	 */
	private $fintsUrl;

	/**
	 * @var string BLZ (Bankleitzahl)
	 */
	private $blz;

	/**
	 * @var string Username
	 */
	private $username;

	/**
	 * @var string PIN (decrypted)
	 */
	private $pin;

	/**
	 * @var string IBAN
	 */
	private $iban;

	/**
	 * @var string FinTS Product ID
	 */
	private $productId;

	/**
	 * @var FinTs|null FinTS instance
	 */
	private $fints = null;

	/**
	 * @var array Available TAN modes
	 */
	public $tanModes = array();

	/**
	 * @var mixed Selected TAN mode
	 */
	public $selectedTanMode = null;

	/**
	 * @var string TAN challenge (for display to user)
	 */
	public $tanChallenge = '';

	/**
	 * @var mixed Current action requiring TAN
	 */
	private $pendingAction = null;

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

		$this->db = $db;

		// Load configuration
		$this->fintsUrl = getDolGlobalString('BANKIMPORT_FINTS_URL');
		$this->blz = getDolGlobalString('BANKIMPORT_FINTS_BLZ');
		$this->username = getDolGlobalString('BANKIMPORT_FINTS_USERNAME');

		// Decrypt PIN
		$encryptedPin = getDolGlobalString('BANKIMPORT_FINTS_PIN');
		if (!empty($encryptedPin)) {
			$this->pin = dolDecrypt($encryptedPin);
		}

		$this->iban = getDolGlobalString('BANKIMPORT_FINTS_IBAN');
		$this->productId = getDolGlobalString('BANKIMPORT_FINTS_PRODUCT_ID');

		// Default product ID if not set
		// Use Hibiscus/Jameica registered product ID
		if (empty($this->productId)) {
			// Official Hibiscus product ID (registered with Deutsche Kreditwirtschaft)
			$this->productId = '36792786FA12F235F04647689';
		}
	}

	/**
	 * Check if configuration is complete
	 *
	 * @return bool True if all required fields are set
	 */
	public function isConfigured()
	{
		return !empty($this->fintsUrl)
			&& !empty($this->blz)
			&& !empty($this->username)
			&& !empty($this->pin)
			&& !empty($this->iban);
	}

	/**
	 * Check if phpFinTS library is available
	 *
	 * @return bool True if library is loaded
	 */
	public function isLibraryAvailable()
	{
		return class_exists('Fhp\FinTs');
	}

	/**
	 * Initialize FinTS connection
	 *
	 * @return int 1 if success, -1 if error
	 */
	public function initConnection()
	{
		if (!$this->isConfigured()) {
			$this->error = 'Configuration incomplete';
			return -1;
		}

		if (!$this->isLibraryAvailable()) {
			$this->error = 'phpFinTS library not found. Run: composer install';
			return -1;
		}

		try {
			$options = new FinTsOptions();
			$options->url = $this->fintsUrl;
			$options->bankCode = $this->blz;
			$options->productName = $this->productId;
			$options->productVersion = '1.0';

			$credentials = Credentials::create($this->username, $this->pin);

			$this->fints = FinTs::new($options, $credentials);

			return 1;
		} catch (Exception $e) {
			$this->error = $e->getMessage();
			return -1;
		}
	}

	/**
	 * Test the FinTS connection
	 *
	 * @return int 1 if success, -1 if error
	 */
	public function testConnection()
	{
		if (!$this->isConfigured()) {
			$this->error = 'Configuration incomplete';
			return -1;
		}

		// Check URL format
		if (!filter_var($this->fintsUrl, FILTER_VALIDATE_URL)) {
			$this->error = 'Invalid FinTS URL format';
			return -1;
		}

		// Check BLZ format (8 digits)
		if (!preg_match('/^\d{8}$/', $this->blz)) {
			$this->error = 'Invalid BLZ format (must be 8 digits)';
			return -1;
		}

		// Check if library is available
		if (!$this->isLibraryAvailable()) {
			$this->error = 'phpFinTS library not installed. Run: cd '.dirname(__DIR__).' && composer install';
			return -1;
		}

		// Try to initialize connection
		$result = $this->initConnection();
		if ($result < 0) {
			return -1;
		}

		try {
			// Try to get TAN modes (this tests the connection)
			$this->tanModes = $this->fints->getTanModes();
			return 1;
		} catch (Exception $e) {
			$this->error = $e->getMessage();
			return -1;
		}
	}

	/**
	 * Get available TAN modes
	 *
	 * @return array Array of TAN modes
	 */
	public function getTanModes()
	{
		if ($this->fints === null) {
			$result = $this->initConnection();
			if ($result < 0) {
				return array();
			}
		}

		try {
			$modes = $this->fints->getTanModes();
			$result = array();
			foreach ($modes as $mode) {
				$result[] = array(
					'id' => $mode->getId(),
					'name' => $mode->getName(),
					'isDecoupled' => $mode->isDecoupled(),
				);
			}
			return $result;
		} catch (Exception $e) {
			$this->error = $e->getMessage();
			return array();
		}
	}

	/**
	 * Login with selected TAN mode
	 *
	 * @param int $tanModeId TAN mode ID to use
	 * @return int 1 if success, 0 if TAN required, -1 if error
	 */
	public function login($tanModeId = null)
	{
		if ($this->fints === null) {
			$result = $this->initConnection();
			if ($result < 0) {
				return -1;
			}
		}

		try {
			// Get and select TAN mode
			$tanModes = $this->fints->getTanModes();

			if ($tanModeId !== null) {
				foreach ($tanModes as $mode) {
					if ($mode->getId() == $tanModeId) {
						$this->selectedTanMode = $mode;
						break;
					}
				}
			} else {
				// Auto-select decoupled mode (SecureGo Plus) if available
				foreach ($tanModes as $mode) {
					if ($mode->isDecoupled()) {
						$this->selectedTanMode = $mode;
						break;
					}
				}
				// Fallback to first mode
				if ($this->selectedTanMode === null && !empty($tanModes)) {
					$this->selectedTanMode = $tanModes[0];
				}
			}

			if ($this->selectedTanMode === null) {
				$this->error = 'No TAN mode available';
				return -1;
			}

			$this->fints->selectTanMode($this->selectedTanMode);

			// Perform login
			$login = $this->fints->login();

			if ($login->needsTan()) {
				$this->pendingAction = $login;
				$tanRequest = $login->getTanRequest();
				$this->tanChallenge = $tanRequest->getChallenge();
				return 0; // TAN required
			}

			return 1;
		} catch (Exception $e) {
			$this->error = $e->getMessage();
			return -1;
		}
	}

	/**
	 * Get pending action for serialization
	 *
	 * @return mixed
	 */
	public function getPendingAction()
	{
		return $this->pendingAction;
	}

	/**
	 * Set pending action after deserialization
	 *
	 * @param mixed $action The pending action
	 * @return void
	 */
	public function setPendingAction($action)
	{
		$this->pendingAction = $action;
	}

	/**
	 * Check if decoupled TAN (SecureGo Plus) has been confirmed
	 *
	 * @return int 1 if confirmed, 0 if still waiting, -1 if error
	 */
	public function checkDecoupledTan()
	{
		if ($this->fints === null) {
			$this->error = 'FinTS instance is null';
			dol_syslog("BankImport: checkDecoupledTan - FinTS instance is null", LOG_ERR);
			return -1;
		}

		if ($this->pendingAction === null) {
			$this->error = 'Pending action is null';
			dol_syslog("BankImport: checkDecoupledTan - Pending action is null", LOG_ERR);
			return -1;
		}

		dol_syslog("BankImport: checkDecoupledTan - Checking TAN status, action type: ".get_class($this->pendingAction), LOG_DEBUG);

		try {
			$done = $this->fints->checkDecoupledSubmission($this->pendingAction);
			if ($done) {
				dol_syslog("BankImport: checkDecoupledTan - TAN confirmed!", LOG_DEBUG);
				$this->pendingAction = null;
				return 1;
			}
			dol_syslog("BankImport: checkDecoupledTan - Still waiting for TAN", LOG_DEBUG);
			return 0;
		} catch (Exception $e) {
			$this->error = $e->getMessage();
			dol_syslog("BankImport: checkDecoupledTan - Exception: ".$e->getMessage(), LOG_ERR);
			return -1;
		}
	}

	/**
	 * Get bank supported parameters (for diagnostics)
	 *
	 * @return array Array of supported parameter segments
	 */
	public function getBankParameters()
	{
		if ($this->fints === null) {
			return array('error' => 'Not connected');
		}

		try {
			// Use reflection to access protected BPD
			$reflection = new \ReflectionClass($this->fints);
			$bpdProperty = $reflection->getProperty('bpd');
			$bpdProperty->setAccessible(true);
			$bpd = $bpdProperty->getValue($this->fints);

			if ($bpd === null) {
				return array('error' => 'BPD not available');
			}

			// Get parameters property from BPD
			$bpdReflection = new \ReflectionClass($bpd);
			$paramsProperty = $bpdReflection->getProperty('parameters');
			$paramsProperty->setAccessible(true);
			$parameters = $paramsProperty->getValue($bpd);

			$result = array();
			foreach ($parameters as $type => $versions) {
				$result[$type] = array_keys($versions);
			}

			return $result;
		} catch (Exception $e) {
			return array('error' => $e->getMessage());
		}
	}

	/**
	 * Submit TAN for pending action
	 *
	 * @param string $tan The TAN entered by user
	 * @return int 1 if success, -1 if error
	 */
	public function submitTan($tan)
	{
		if ($this->fints === null || $this->pendingAction === null) {
			$this->error = 'No pending action';
			return -1;
		}

		try {
			$this->fints->submitTan($this->pendingAction, $tan);
			$this->pendingAction = null;
			return 1;
		} catch (Exception $e) {
			$this->error = $e->getMessage();
			return -1;
		}
	}

	/**
	 * Get SEPA accounts
	 *
	 * @return array|int Array of accounts or -1 on error
	 */
	public function getAccounts()
	{
		if ($this->fints === null) {
			$this->error = 'Not connected';
			return -1;
		}

		try {
			$getAccounts = GetSEPAAccounts::create();
			$this->fints->execute($getAccounts);

			if ($getAccounts->needsTan()) {
				$this->pendingAction = $getAccounts;
				$tanRequest = $getAccounts->getTanRequest();
				$this->tanChallenge = $tanRequest->getChallenge();
				return 0; // TAN required
			}

			$accounts = $getAccounts->getAccounts();
			$result = array();

			foreach ($accounts as $account) {
				$result[] = array(
					'iban' => $account->getIban(),
					'bic' => $account->getBic(),
					'accountNumber' => $account->getAccountNumber(),
					'blz' => $account->getBlz(),
				);
			}

			return $result;
		} catch (Exception $e) {
			$this->error = $e->getMessage();
			return -1;
		}
	}

	/**
	 * Maximum days per fetch request (banks often limit this)
	 */
	const MAX_DAYS_PER_FETCH = 30;

	/**
	 * Fetch account statements
	 *
	 * @param int $dateFrom Start date (timestamp)
	 * @param int $dateTo   End date (timestamp)
	 * @return array|int Array of transactions or -1 on error, 0 if TAN required
	 */
	public function fetchStatements($dateFrom = 0, $dateTo = 0)
	{
		if (!$this->isConfigured()) {
			$this->error = 'Configuration incomplete';
			return -1;
		}

		if ($this->fints === null) {
			$this->error = 'Not connected. Call login() first.';
			return -1;
		}

		// Default: last 30 days
		if (empty($dateFrom)) {
			$dateFrom = strtotime('-30 days');
		}
		if (empty($dateTo)) {
			$dateTo = time();
		}

		// Check if date range exceeds limit - if so, fetch in chunks
		$daysDiff = ($dateTo - $dateFrom) / 86400;
		if ($daysDiff > self::MAX_DAYS_PER_FETCH) {
			dol_syslog("BankImport: Date range {$daysDiff} days exceeds limit, fetching in chunks", LOG_DEBUG);
			return $this->fetchStatementsInChunks($dateFrom, $dateTo);
		}

		return $this->fetchStatementsForPeriod($dateFrom, $dateTo);
	}

	/**
	 * Fetch statements in chunks for long date ranges
	 *
	 * @param int $dateFrom Start date (timestamp)
	 * @param int $dateTo   End date (timestamp)
	 * @return array|int Array of transactions or -1 on error, 0 if TAN required
	 */
	protected function fetchStatementsInChunks($dateFrom, $dateTo)
	{
		$allTransactions = array();
		$lastBalance = array();
		$chunkDays = self::MAX_DAYS_PER_FETCH;
		$oldestDateReached = null;

		// Start from the most recent date and go backwards
		// This ensures we get the newest transactions even if old ones aren't available
		$currentTo = $dateTo;

		while ($currentTo > $dateFrom) {
			$currentFrom = max($currentTo - ($chunkDays * 86400), $dateFrom);

			dol_syslog("BankImport: Fetching chunk from ".date('Y-m-d', $currentFrom)." to ".date('Y-m-d', $currentTo), LOG_DEBUG);

			$result = $this->fetchStatementsForPeriod($currentFrom, $currentTo);

			if ($result === 0) {
				// TAN required - save progress and return
				$_SESSION['fints_chunk_progress'] = array(
					'transactions' => $allTransactions,
					'currentTo' => $currentFrom,
					'dateFrom' => $dateFrom
				);
				return 0;
			}

			if ($result < 0) {
				// Error fetching this chunk
				// If we already have transactions from newer chunks, return those with a note
				if (!empty($allTransactions)) {
					dol_syslog("BankImport: Chunk failed for older dates, returning ".count($allTransactions)." transactions from recent period", LOG_WARNING);
					return array(
						'transactions' => $allTransactions,
						'balance' => $lastBalance,
						'partial' => true,
						'oldestDate' => $oldestDateReached,
						'info' => 'Ältere Daten (vor '.date('d.m.Y', $currentTo).') sind bei der Bank nicht mehr verfügbar.'
					);
				}
				return -1;
			}

			if (is_array($result)) {
				$newTransactions = $result['transactions'] ?? array();
				$allTransactions = array_merge($allTransactions, $newTransactions);

				// Track oldest date we successfully fetched
				foreach ($newTransactions as $tx) {
					if ($oldestDateReached === null || $tx['date'] < $oldestDateReached) {
						$oldestDateReached = $tx['date'];
					}
				}

				if (empty($lastBalance) && !empty($result['balance'])) {
					$lastBalance = $result['balance'];
				}
			}

			$currentTo = $currentFrom - 86400; // Previous day
		}

		// Remove duplicates based on transaction ID
		$uniqueTransactions = array();
		$seenIds = array();
		foreach ($allTransactions as $tx) {
			if (!isset($seenIds[$tx['id']])) {
				$uniqueTransactions[] = $tx;
				$seenIds[$tx['id']] = true;
			}
		}

		// Sort by date descending (newest first)
		usort($uniqueTransactions, function($a, $b) {
			return $b['date'] - $a['date'];
		});

		dol_syslog("BankImport: Total fetched: ".count($uniqueTransactions)." unique transactions", LOG_DEBUG);

		return array(
			'transactions' => $uniqueTransactions,
			'balance' => $lastBalance
		);
	}

	/**
	 * Fetch statements for a single period (internal)
	 *
	 * @param int $dateFrom Start date (timestamp)
	 * @param int $dateTo   End date (timestamp)
	 * @return array|int Array of transactions or -1 on error, 0 if TAN required
	 */
	protected function fetchStatementsForPeriod($dateFrom, $dateTo)
	{
		try {
			// Get accounts first
			$getAccounts = GetSEPAAccounts::create();
			$this->fints->execute($getAccounts);

			if ($getAccounts->needsTan()) {
				$this->pendingAction = $getAccounts;
				$tanRequest = $getAccounts->getTanRequest();
				$this->tanChallenge = $tanRequest->getChallenge();
				return 0;
			}

			$accounts = $getAccounts->getAccounts();

			// Debug: Log all available accounts
			dol_syslog("BankImport: Looking for IBAN: ".$this->iban, LOG_DEBUG);
			foreach ($accounts as $idx => $acc) {
				dol_syslog("BankImport: Available account ".$idx.": IBAN=".$acc->getIban(), LOG_DEBUG);
			}

			// Find matching account by IBAN
			$selectedAccount = null;
			foreach ($accounts as $account) {
				if ($account->getIban() === $this->iban) {
					$selectedAccount = $account;
					dol_syslog("BankImport: Selected account by IBAN match: ".$account->getIban(), LOG_DEBUG);
					break;
				}
			}

			if ($selectedAccount === null) {
				// Fallback: use first account
				if (!empty($accounts)) {
					$selectedAccount = $accounts[0];
					dol_syslog("BankImport: WARNING - No IBAN match found! Using first account: ".$selectedAccount->getIban(), LOG_WARNING);
				} else {
					$this->error = 'No accounts found';
					return -1;
				}
			}

			// Fetch statements
			$from = new DateTime();
			$from->setTimestamp($dateFrom);
			$to = new DateTime();
			$to->setTimestamp($dateTo);

			$transactions = array();

			// Log what we're trying
			dol_syslog("BankImport: Fetching statements from ".$from->format('Y-m-d')." to ".$to->format('Y-m-d'), LOG_DEBUG);

			// Try GetStatementOfAccount first (MT940/HKKAZ - most widely supported)
			$mt940Success = false;
			try {
				dol_syslog("BankImport: Trying GetStatementOfAccount (MT940/HKKAZ)", LOG_DEBUG);
				$getStatement = GetStatementOfAccount::create($selectedAccount, $from, $to);
				$this->fints->execute($getStatement);

				if ($getStatement->needsTan()) {
					$this->pendingAction = $getStatement;
					$tanRequest = $getStatement->getTanRequest();
					$this->tanChallenge = $tanRequest->getChallenge();
					return 0;
				}

				// Process MT940 results
				$soa = $getStatement->getStatement();
				foreach ($soa->getStatements() as $statement) {
					foreach ($statement->getTransactions() as $tx) {
						$sign = ($tx->getCreditDebit() === Transaction::CD_DEBIT) ? -1 : 1;

						$transactions[] = array(
							'id' => md5($tx->getValutaDate()->format('Y-m-d').$tx->getAmount().$tx->getName().$tx->getMainDescription()),
							'date' => $tx->getValutaDate()->getTimestamp(),
							'bookingDate' => $tx->getBookingDate() ? $tx->getBookingDate()->getTimestamp() : null,
							'amount' => $sign * $tx->getAmount(),
							'currency' => 'EUR',
							'name' => $tx->getName(),
							'iban' => $tx->getAccountNumber(),
							'bic' => $tx->getBankCode(),
							'reference' => $tx->getMainDescription(),
							'bookingText' => $tx->getBookingText(),
							'endToEndId' => $tx->getEndToEndID(),
						);
					}
				}
				dol_syslog("BankImport: MT940 successful, got ".count($transactions)." transactions", LOG_DEBUG);

				// Return MT940 results
				return array(
					'transactions' => $transactions,
					'balance' => array()
				);
			} catch (Exception $e) {
				dol_syslog("BankImport: MT940 failed: ".$e->getMessage(), LOG_DEBUG);

				// MT940 failed, try CAMT/XML
				dol_syslog("BankImport: Trying GetStatementOfAccountXML (CAMT)", LOG_DEBUG);
				try {
					$getStatementXML = GetStatementOfAccountXML::create($selectedAccount, $from, $to);
					$this->fints->execute($getStatementXML);

				if ($getStatementXML->needsTan()) {
					$this->pendingAction = $getStatementXML;
					$tanRequest = $getStatementXML->getTanRequest();
					$this->tanChallenge = $tanRequest->getChallenge();
					return 0;
				}

				// Process CAMT XML results
				$xmlStatements = $getStatementXML->getBookedXML();

				// Debug: Log raw XML count
				dol_syslog("BankImport: Got ".count($xmlStatements)." XML documents", LOG_DEBUG);

				// Balance info from XML
				$balanceInfo = array();

				foreach ($xmlStatements as $idx => $camtDoc) {
					// Debug: Save raw XML to temp file for analysis
					$debugFile = DOL_DATA_ROOT.'/bankimport_debug_'.$idx.'.xml';
					file_put_contents($debugFile, $camtDoc);
					dol_syslog("BankImport: Saved XML to ".$debugFile, LOG_DEBUG);

					// Parse CAMT XML
					$xml = simplexml_load_string($camtDoc);
					if ($xml === false) {
						dol_syslog("BankImport: Failed to parse XML document ".$idx, LOG_ERR);
						continue;
					}

					// Get all namespaces used in the document
					$namespaces = $xml->getNamespaces(true);
					dol_syslog("BankImport: Namespaces: ".json_encode($namespaces), LOG_DEBUG);

					// Register all found namespaces
					foreach ($namespaces as $prefix => $uri) {
						if (empty($prefix)) {
							$xml->registerXPathNamespace('ns', $uri);
						} else {
							$xml->registerXPathNamespace($prefix, $uri);
						}
					}

					// Extract balance information from XML
					$balances = $xml->xpath('//ns:Bal') ?: $xml->xpath('//Bal') ?: $xml->xpath('//*[local-name()="Bal"]') ?: [];
					foreach ($balances as $bal) {
						$balType = (string) ($bal->Tp->CdOrPrtry->Cd ?? '');
						if ($balType === 'CLBD') { // Closing balance
							$balAmount = (float) ($bal->Amt ?? 0);
							$balCcy = (string) ($bal->Amt['Ccy'] ?? 'EUR');
							$balSign = ((string) ($bal->CdtDbtInd ?? 'CRDT')) === 'DBIT' ? -1 : 1;
							$balDate = (string) ($bal->Dt->Dt ?? date('Y-m-d'));
							$balanceInfo = array(
								'amount' => $balSign * $balAmount,
								'currency' => $balCcy,
								'date' => $balDate,
								'type' => 'CLBD'
							);
							dol_syslog("BankImport: Found closing balance: ".$balanceInfo['amount']." ".$balCcy." at ".$balDate, LOG_DEBUG);
						}
					}

					// Try multiple XPath patterns for different CAMT versions
					$entries = $xml->xpath('//ns:Ntry') ?: $xml->xpath('//Ntry') ?: $xml->xpath('//*[local-name()="Ntry"]') ?: [];
					dol_syslog("BankImport: Found ".count($entries)." entries", LOG_DEBUG);

					foreach ($entries as $entry) {
						$amount = (float) ($entry->Amt ?? 0);
						$sign = ((string) ($entry->CdtDbtInd ?? 'CRDT')) === 'DBIT' ? -1 : 1;
						$date = (string) ($entry->BookgDt->Dt ?? $entry->ValDt->Dt ?? date('Y-m-d'));

						// Get counterparty name - CAMT.052.001.08 has Pty wrapper
						$txDtls = $entry->NtryDtls->TxDtls ?? null;
						$name = '';
						if ($txDtls) {
							// For DBIT (outgoing): counterparty is Cdtr (creditor)
							// For CRDT (incoming): counterparty is Dbtr (debtor)
							if ($sign < 0) {
								// Outgoing payment - get creditor name
								$name = (string) ($txDtls->RltdPties->Cdtr->Pty->Nm
									?? $txDtls->RltdPties->Cdtr->Nm
									?? '');
							} else {
								// Incoming payment - get debtor name
								$name = (string) ($txDtls->RltdPties->Dbtr->Pty->Nm
									?? $txDtls->RltdPties->Dbtr->Nm
									?? '');
							}
						}

						// Get reference/description
						$reference = '';
						if ($txDtls && isset($txDtls->RmtInf)) {
							$reference = (string) ($txDtls->RmtInf->Ustrd ?? '');
						}
						// Fallback to AddtlNtryInf
						if (empty($reference)) {
							$reference = (string) ($entry->AddtlNtryInf ?? '');
						}

						$transactions[] = array(
							'id' => md5($date . $amount . $name . $reference),
							'date' => strtotime($date),
							'bookingDate' => strtotime($date),
							'amount' => $sign * $amount,
							'currency' => (string) ($entry->Amt['Ccy'] ?? 'EUR'),
							'name' => $name,
							'iban' => '',
							'bic' => '',
							'reference' => $reference,
							'bookingText' => (string) ($entry->AddtlNtryInf ?? ''),
							'endToEndId' => (string) ($txDtls->Refs->EndToEndId ?? ''),
						);
					}
				}

				// Return transactions with balance info
				return array(
					'transactions' => $transactions,
					'balance' => $balanceInfo
				);

			} catch (Exception $e2) {
				// CAMT also failed - both methods failed
				// Check if this is likely a historical data limit issue
				$daysAgo = (time() - $dateFrom) / 86400;
				$errorMsg = $e->getMessage() . ' ' . $e2->getMessage();

				// Banks typically don't provide data older than 90 days
				// Error messages containing "HIKAZS" or "not support" often indicate data unavailability
				if ($daysAgo > 60 && (
					stripos($errorMsg, 'HIKAZS') !== false ||
					stripos($errorMsg, 'not support') !== false ||
					stripos($errorMsg, 'nicht unterstützt') !== false ||
					stripos($errorMsg, 'nicht verfügbar') !== false
				)) {
					$this->error = 'Die Bank stellt historische Kontodaten nur für ca. 2-3 Monate bereit. Bitte wählen Sie einen kürzeren Zeitraum.';
				} else {
					$this->error = 'Abruf fehlgeschlagen: ' . $e->getMessage();
				}
				dol_syslog("BankImport: Both MT940 and CAMT failed. MT940: ".$e->getMessage()." | CAMT: ".$e2->getMessage(), LOG_ERR);
				return -1;
			}
			}
		} catch (Exception $e) {
			$this->error = $e->getMessage();
			return -1;
		}
	}

	/**
	 * Close FinTS connection
	 *
	 * @return void
	 */
	public function close()
	{
		if ($this->fints !== null) {
			try {
				$this->fints->close();
			} catch (Exception $e) {
				// Ignore close errors
			}
			$this->fints = null;
		}
	}

	/**
	 * Persist FinTS state (for web session handling)
	 *
	 * @return string|null Serialized state or null
	 */
	public function persist()
	{
		if ($this->fints === null) {
			return null;
		}
		try {
			return $this->fints->persist();
		} catch (Exception $e) {
			return null;
		}
	}

	/**
	 * Restore FinTS from persisted state
	 *
	 * @param string $state Persisted state
	 * @return int 1 if success, -1 if error
	 */
	public function restore($state)
	{
		if (!$this->isConfigured()) {
			$this->error = 'Configuration incomplete';
			return -1;
		}

		if (!$this->isLibraryAvailable()) {
			$this->error = 'phpFinTS library not found';
			return -1;
		}

		if (empty($state)) {
			$this->error = 'Empty state provided';
			dol_syslog("BankImport: restore - Empty state provided", LOG_ERR);
			return -1;
		}

		dol_syslog("BankImport: restore - Restoring FinTS state, length=".strlen($state), LOG_DEBUG);

		try {
			$options = new FinTsOptions();
			$options->url = $this->fintsUrl;
			$options->bankCode = $this->blz;
			$options->productName = $this->productId;
			$options->productVersion = '1.0';

			$credentials = Credentials::create($this->username, $this->pin);

			$this->fints = FinTs::new($options, $credentials, $state);

			dol_syslog("BankImport: restore - FinTS state restored successfully", LOG_DEBUG);
			return 1;
		} catch (Exception $e) {
			$this->error = $e->getMessage();
			dol_syslog("BankImport: restore - Exception: ".$e->getMessage(), LOG_ERR);
			return -1;
		}
	}

	/**
	 * Get FinTS URL
	 *
	 * @return string
	 */
	public function getFintsUrl()
	{
		return $this->fintsUrl;
	}

	/**
	 * Get BLZ
	 *
	 * @return string
	 */
	public function getBlz()
	{
		return $this->blz;
	}

	/**
	 * Get IBAN
	 *
	 * @return string
	 */
	public function getIban()
	{
		return $this->iban;
	}

	/**
	 * Get PDF bank statement via HKEKP
	 *
	 * @param int $accountIndex Index of account to use (default 0)
	 * @param int|null $statementNumber Optional: specific statement number
	 * @param int|null $statementYear Optional: statement year
	 * @return array|int Array with 'pdfData' and 'info', or 0 if TAN required, or -1 on error
	 */
	public function getStatementPDF($accountIndex = 0, $statementNumber = null, $statementYear = null)
	{
		global $conf;

		$this->error = '';

		if (!$this->fints) {
			$this->error = 'Not connected';
			return -1;
		}

		try {
			// Get accounts if not cached
			if (empty($this->accounts)) {
				$getAccounts = GetSEPAAccounts::create();
				$this->fints->execute($getAccounts);

				if ($getAccounts->needsTan()) {
					$this->pendingAction = $getAccounts;
					$tanRequest = $getAccounts->getTanRequest();
					$this->tanChallenge = $tanRequest->getChallenge();
					return 0;
				}

				$this->accounts = $getAccounts->getAccounts();
			}

			if (empty($this->accounts) || !isset($this->accounts[$accountIndex])) {
				$this->error = 'No accounts available or invalid account index';
				return -1;
			}

			$selectedAccount = $this->accounts[$accountIndex];

			dol_syslog("BankImport: Fetching PDF statement via HKEKP", LOG_DEBUG);

			$getPdf = GetStatementPDF::create(
				$selectedAccount,
				$statementNumber,
				$statementYear
			);

			$this->fints->execute($getPdf);

			if ($getPdf->needsTan()) {
				$this->pendingAction = $getPdf;
				$tanRequest = $getPdf->getTanRequest();
				$this->tanChallenge = $tanRequest->getChallenge();
				return 0;
			}

			$pdfData = $getPdf->getPdfData();
			$info = $getPdf->getStatementInfo();

			if (empty($pdfData)) {
				dol_syslog("BankImport: No PDF data received (no new statements available)", LOG_DEBUG);
				return array(
					'pdfData' => '',
					'info' => array(),
					'message' => 'No new statements available'
				);
			}

			dol_syslog("BankImport: Received PDF statement #".$info['statementNumber'].'/'.$info['statementYear'], LOG_DEBUG);

			return array(
				'pdfData' => $pdfData,
				'info' => $info
			);

		} catch (Exception $e) {
			$this->error = $e->getMessage();
			dol_syslog("BankImport: HKEKP failed: ".$this->error, LOG_ERR);

			// Check if bank doesn't support HKEKP
			if (stripos($this->error, 'HKEKP') !== false || stripos($this->error, 'not support') !== false) {
				$this->error = 'Bank does not support PDF statements (HKEKP)';
			}

			return -1;
		}
	}

	/**
	 * Check if bank supports PDF statements (HKEKP)
	 *
	 * @return bool
	 */
	public function supportsPdfStatements()
	{
		if (!$this->fints) {
			return false;
		}

		try {
			// Try to get BPD and check for HIEKPS
			$reflection = new ReflectionClass($this->fints);
			$bpdProperty = $reflection->getProperty('bpd');
			$bpdProperty->setAccessible(true);
			$bpd = $bpdProperty->getValue($this->fints);

			if ($bpd === null) {
				return false;
			}

			$hiekps = $bpd->getLatestSupportedParameters('HIEKPS');
			return $hiekps !== null;

		} catch (Exception $e) {
			return false;
		}
	}

	/**
	 * Check if bank supports archive statements (HKKAA) with PDF format
	 *
	 * @return bool
	 */
	public function supportsArchiveStatements()
	{
		if (!$this->fints) {
			return false;
		}

		try {
			$reflection = new ReflectionClass($this->fints);
			$bpdProperty = $reflection->getProperty('bpd');
			$bpdProperty->setAccessible(true);
			$bpd = $bpdProperty->getValue($this->fints);

			if ($bpd === null) {
				return false;
			}

			$hikaas = $bpd->getLatestSupportedParameters('HIKAAS');
			if ($hikaas === null) {
				return false;
			}

			// Check if PDF format is supported
			$param = $hikaas->getParameter();
			return $param->supportsPdf();

		} catch (Exception $e) {
			dol_syslog("BankImport: supportsArchiveStatements exception: ".$e->getMessage(), LOG_DEBUG);
			return false;
		}
	}

	/**
	 * Check if bank supports any PDF statement method (HKEKP or HKKAA)
	 *
	 * @return string|false Method name ('HKEKP', 'HKKAA') or false if none supported
	 */
	public function getPdfStatementMethod()
	{
		// First try HKEKP (direct PDF)
		if ($this->supportsPdfStatements()) {
			return 'HKEKP';
		}

		// Fallback to HKKAA (archive)
		if ($this->supportsArchiveStatements()) {
			return 'HKKAA';
		}

		return false;
	}

	/**
	 * Get PDF bank statement from archive via HKKAA
	 *
	 * @param int $accountIndex Index of account to use (default 0)
	 * @param \DateTime|null $fromDate Optional: start date
	 * @param \DateTime|null $toDate Optional: end date
	 * @return array|int Array with 'pdfData' and 'info', or 0 if TAN required, or -1 on error
	 */
	public function getStatementFromArchive($accountIndex = 0, $fromDate = null, $toDate = null)
	{
		global $conf;

		$this->error = '';

		if (!$this->fints) {
			$this->error = 'Not connected';
			return -1;
		}

		try {
			// Get accounts if not cached
			if (empty($this->accounts)) {
				$getAccounts = GetSEPAAccounts::create();
				$this->fints->execute($getAccounts);

				if ($getAccounts->needsTan()) {
					$this->pendingAction = $getAccounts;
					$tanRequest = $getAccounts->getTanRequest();
					$this->tanChallenge = $tanRequest->getChallenge();
					return 0;
				}

				$this->accounts = $getAccounts->getAccounts();
			}

			if (empty($this->accounts) || !isset($this->accounts[$accountIndex])) {
				$this->error = 'No accounts available or invalid account index';
				return -1;
			}

			$selectedAccount = $this->accounts[$accountIndex];

			dol_syslog("BankImport: Fetching PDF statement from archive via HKKAA", LOG_DEBUG);

			$getArchive = GetStatementFromArchive::create(
				$selectedAccount,
				GetStatementFromArchive::FORMAT_PDF,
				$fromDate,
				$toDate
			);

			$this->fints->execute($getArchive);

			if ($getArchive->needsTan()) {
				$this->pendingAction = $getArchive;
				$tanRequest = $getArchive->getTanRequest();
				$this->tanChallenge = $tanRequest->getChallenge();
				return 0;
			}

			$pdfData = $getArchive->getPdfData();
			$info = $getArchive->getStatementInfo();

			if (empty($pdfData)) {
				dol_syslog("BankImport: No PDF data received from archive", LOG_DEBUG);
				return array(
					'pdfData' => '',
					'info' => array(),
					'message' => 'No statements available in archive'
				);
			}

			dol_syslog("BankImport: Received PDF statement from archive #".($info['statementNumber'] ?? '?').'/'.($info['statementYear'] ?? '?'), LOG_DEBUG);

			return array(
				'pdfData' => $pdfData,
				'info' => $info
			);

		} catch (Exception $e) {
			$this->error = $e->getMessage();
			dol_syslog("BankImport: HKKAA failed: ".$this->error, LOG_ERR);
			dol_syslog("BankImport: HKKAA Exception class: ".get_class($e), LOG_DEBUG);

			// Keep original error message for better debugging
			return -1;
		}
	}

	/**
	 * Get PDF statement using best available method (HKEKP or HKKAA)
	 *
	 * @param int $accountIndex Index of account to use (default 0)
	 * @param int|null $statementNumber Optional: specific statement number (only for HKEKP)
	 * @param int|null $statementYear Optional: statement year (only for HKEKP)
	 * @return array|int Array with 'pdfData', 'info' and 'method', or 0 if TAN required, or -1 on error
	 */
	public function getStatementPDFAuto($accountIndex = 0, $statementNumber = null, $statementYear = null)
	{
		$method = $this->getPdfStatementMethod();

		if ($method === false) {
			$this->error = 'Bank does not support PDF statements (neither HKEKP nor HKKAA)';
			return -1;
		}

		if ($method === 'HKEKP') {
			$result = $this->getStatementPDF($accountIndex, $statementNumber, $statementYear);
		} else {
			// HKKAA doesn't support statement number, use date range instead
			$result = $this->getStatementFromArchive($accountIndex);
		}

		if (is_array($result)) {
			$result['method'] = $method;
		}

		return $result;
	}
}
