<?php
/* Copyright (C) 2026 ZUGFeRD Import Module
 *
 * 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    class/cron_importzugferd.class.php
 * \ingroup importzugferd
 * \brief   Cron job class for fetching ZUGFeRD invoices from mailbox
 */

require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';

dol_include_once('/importzugferd/class/zugferdparser.class.php');
dol_include_once('/importzugferd/class/zugferdimport.class.php');
dol_include_once('/importzugferd/class/actions_importzugferd.class.php');

/**
 * Class CronImportZugferd
 * Cronjob handler for fetching ZUGFeRD invoices from IMAP mailbox and folder
 */
class CronImportZugferd
{
    /**
     * @var DoliDB Database handler
     */
    public $db;

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

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

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

    /**
     * @var int Number of imported invoices
     */
    public $imported_count = 0;

    /**
     * @var int Number of skipped invoices (duplicates)
     */
    public $skipped_count = 0;

    /**
     * @var int Number of errors
     */
    public $error_count = 0;

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

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

    /**
     * Send notification via GlobalNotify (if available)
     *
     * @param string $type       'error', 'warning', 'info', 'action'
     * @param string $title      Title
     * @param string $message    Message
     * @param string $actionUrl  URL for action button
     * @param string $actionLabel Label for action button
     * @return bool True if sent via GlobalNotify
     */
    protected function notify($type, $title, $message, $actionUrl = '', $actionLabel = '')
    {
        if (!isModEnabled('globalnotify')) {
            dol_syslog("ImportZugferd [{$type}]: {$title} - {$message}", LOG_WARNING);
            return false;
        }

        $classFile = dol_buildpath('/globalnotify/class/globalnotify.class.php', 0);
        if (!file_exists($classFile)) {
            dol_syslog("ImportZugferd [{$type}]: {$title} - {$message}", LOG_WARNING);
            return false;
        }

        require_once $classFile;

        if (!class_exists('GlobalNotify')) {
            dol_syslog("ImportZugferd [{$type}]: {$title} - {$message}", LOG_WARNING);
            return false;
        }

        try {
            switch ($type) {
                case 'error':
                    GlobalNotify::error('importzugferd', $title, $message, $actionUrl, $actionLabel);
                    break;
                case 'warning':
                    GlobalNotify::warning('importzugferd', $title, $message, $actionUrl, $actionLabel);
                    break;
                case 'action':
                    GlobalNotify::actionRequired('importzugferd', $title, $message, $actionUrl, $actionLabel ?: 'Aktion erforderlich');
                    break;
                default:
                    GlobalNotify::info('importzugferd', $title, $message, $actionUrl, $actionLabel);
            }
            return true;
        } catch (Exception $e) {
            dol_syslog("GlobalNotify error: ".$e->getMessage(), LOG_ERR);
            return false;
        }
    }

    /**
     * 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->importzugferd->dir_output.'/logs';
        if (!is_dir($logDir)) {
            dol_mkdir($logDir);
        }
        $this->cronLogFile = $logDir.'/cron_importzugferd.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";

        @file_put_contents($this->cronLogFile, $logLine, FILE_APPEND | LOCK_EX);
        dol_syslog("CronImportZugferd: ".$message, $level === 'ERROR' ? LOG_ERR : ($level === 'WARNING' ? LOG_WARNING : LOG_INFO));
    }

    /**
     * Shutdown handler to catch fatal errors
     */
    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->cronLog("========== CRON END (fatal shutdown) ==========");
        }
    }

    /**
     * Check if import should run based on configured frequency
     *
     * @return bool True if import should run
     */
    protected function shouldRunImport()
    {
        global $conf;

        $frequency = getDolGlobalString('IMPORTZUGFERD_IMPORT_FREQUENCY', 'manual');

        if ($frequency === 'manual') {
            return false;
        }

        // Get last run timestamp
        $lastRun = getDolGlobalInt('IMPORTZUGFERD_LAST_IMPORT_RUN', 0);
        $now = dol_now();

        // Calculate minimum interval based on frequency
        $interval = 0;
        switch ($frequency) {
            case 'hourly':
                $interval = 3600; // 1 hour
                break;
            case 'daily':
                $interval = 86400; // 24 hours
                break;
            case 'weekly':
                $interval = 604800; // 7 days
                break;
        }

        // Check if enough time has passed
        if ($now - $lastRun < $interval) {
            return false;
        }

        return true;
    }

    /**
     * Update last run timestamp
     */
    protected function updateLastRunTime()
    {
        global $conf;
        dolibarr_set_const($this->db, 'IMPORTZUGFERD_LAST_IMPORT_RUN', dol_now(), 'chaine', 0, '', $conf->entity);
    }

    /**
     * Main import method - imports from both folder and mailbox
     *
     * @return int 0 if OK, <0 if error
     */
    public function runScheduledImport()
    {
        global $langs;

        // Initialize timing and shutdown handler
        $this->startTime = microtime(true);
        register_shutdown_function(array($this, 'handleShutdown'));

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

        $this->cronLog("========== CRON START ==========");
        $this->cronLog("PHP Version: ".PHP_VERSION.", Memory Limit: ".ini_get('memory_limit'));

        // Reset counters
        $this->imported_count = 0;
        $this->skipped_count = 0;
        $this->error_count = 0;
        $this->errors = array();

        try {
            $this->cronLog("Starting folder import...");
            $this->importFromFolder();
            $this->cronLog("Folder import completed");

            // IMAP nur wenn konfiguriert
            if (!empty(getDolGlobalString('IMPORTZUGFERD_IMAP_HOST'))) {
                $this->cronLog("Starting IMAP import...");
                $this->fetchFromMailbox();
                $this->cronLog("IMAP import completed");
            } else {
                $this->cronLog("IMAP not configured - skipping");
            }

            // Update last run time
            $this->updateLastRunTime();

            // Build combined output
            $this->output = sprintf(
                "Scheduled import complete. Imported: %d, Skipped: %d, Errors: %d",
                $this->imported_count,
                $this->skipped_count,
                $this->error_count
            );

            if ($this->error_count > 0 && !empty($this->errors)) {
                $this->output .= "\nErrors: " . implode(", ", array_slice($this->errors, 0, 5));
            }

            $duration = round(microtime(true) - $this->startTime, 2);
            $this->cronLog("Completed: imported={$this->imported_count}, skipped={$this->skipped_count}, errors={$this->error_count}, duration={$duration}s");
            $this->cronLog("========== CRON END (success) ==========");

            // Send GlobalNotify notifications
            $this->sendImportNotifications();

            return ($this->error_count > 0) ? -1 : 0;

        } catch (Exception $e) {
            $this->error = 'Exception: '.$e->getMessage();
            $this->cronLog("EXCEPTION: ".$e->getMessage()."\n".$e->getTraceAsString(), 'ERROR');
            $this->cronLog("========== CRON END (exception) ==========");
            $this->notify(
                'error',
                'ZUGFeRD Import fehlgeschlagen',
                'Exception: '.$e->getMessage(),
                dol_buildpath('/importzugferd/admin/setup.php', 1),
                'Einstellungen prüfen'
            );
            return -1;
        } catch (Throwable $t) {
            $this->error = 'Fatal: '.$t->getMessage();
            $this->cronLog("FATAL: ".$t->getMessage()."\n".$t->getTraceAsString(), 'ERROR');
            $this->cronLog("========== CRON END (fatal) ==========");
            $this->notify(
                'error',
                'ZUGFeRD Import Absturz',
                'Fatal: '.$t->getMessage(),
                dol_buildpath('/importzugferd/admin/setup.php', 1),
                'Einstellungen prüfen'
            );
            return -1;
        }
    }

    /**
     * Import ZUGFeRD invoices from watch folder
     *
     * @return int 0 if OK, <0 if error
     */
    public function importFromFolder()
    {
        global $langs;

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

        $watchFolder = getDolGlobalString('IMPORTZUGFERD_WATCH_FOLDER');
        $archiveFolder = getDolGlobalString('IMPORTZUGFERD_ARCHIVE_FOLDER');
        $errorFolder = getDolGlobalString('IMPORTZUGFERD_ERROR_FOLDER');
        $autoCreate = getDolGlobalString('IMPORTZUGFERD_AUTO_CREATE_INVOICE');

        $this->cronLog("Watch folder: {$watchFolder}");

        // Validate settings
        if (empty($watchFolder)) {
            $this->cronLog("Watch folder not configured - skipping");
            $this->output = 'Watch folder not configured';
            return 0;
        }

        if (!is_dir($watchFolder)) {
            $this->cronLog("Watch folder not accessible: {$watchFolder}", 'WARNING');
            $this->output = 'Watch folder not accessible';
            return 0;
        }

        $this->cronLog("Watch folder accessible, scanning for PDFs...");

        // Load admin user for import actions
        $admin_user = new User($this->db);
        $admin_user->fetch(1);

        // Find PDF files
        $this->cronLog("Running glob for *.pdf...");
        $files = glob($watchFolder . '/*.pdf');
        $this->cronLog("Found ".count($files)." .pdf files");

        $this->cronLog("Running glob for *.PDF...");
        $filesUpper = glob($watchFolder . '/*.PDF');
        $this->cronLog("Found ".count($filesUpper)." .PDF files");

        $files = array_merge($files, $filesUpper);

        if (empty($files)) {
            $this->cronLog("No PDF files found in watch folder");
            return 0;
        }

        $this->cronLog("Total ".count($files)." PDF files to process");

        // Ensure archive folder exists if configured
        if (!empty($archiveFolder) && !is_dir($archiveFolder)) {
            dol_mkdir($archiveFolder);
        }

        // Ensure error folder exists if configured
        if (!empty($errorFolder) && !is_dir($errorFolder)) {
            dol_mkdir($errorFolder);
        }

        foreach ($files as $file) {
            $this->cronLog("Processing: ".basename($file));

            // Use ZugferdImport::importFromFile for consistent handling
            $import = new ZugferdImport($this->db);
            $result = $import->importFromFile($admin_user, $file, !empty($autoCreate));

            if ($result > 0) {
                $this->imported_count++;
                $this->cronLog("Imported: ".basename($file)." -> ID {$result}");

                // Archive the file
                $this->moveFile($file, $archiveFolder, 'imported_');
            } elseif ($result == -2) {
                // Duplicate - already imported
                $this->skipped_count++;
                $this->cronLog("Skipped (duplicate): ".basename($file));

                // Archive duplicates - delete if no archive folder
                if (!$this->moveFile($file, $archiveFolder, 'duplicate_')) {
                    @unlink($file);
                }
            } else {
                $this->error_count++;
                $this->errors[] = basename($file) . ': ' . $import->error;
                $this->cronLog("Error importing ".basename($file).": ".$import->error, 'ERROR');

                // Try error folder first, fall back to archive folder
                if (!$this->moveFile($file, $errorFolder, 'error_')) {
                    // Use archive folder as fallback for errors
                    $this->moveFile($file, $archiveFolder, 'error_');
                }
            }
        }

        $this->cronLog("Folder import finished: imported={$this->imported_count}, skipped={$this->skipped_count}, errors={$this->error_count}");
        return 0;
    }

    /**
     * Fetch ZUGFeRD invoices from configured IMAP mailbox
     *
     * @return int 0 if OK, <0 if error
     */
    public function fetchFromMailbox()
    {
        global $conf, $user, $langs;

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

        // Get IMAP settings
        $host = getDolGlobalString('IMPORTZUGFERD_IMAP_HOST');
        $port = getDolGlobalString('IMPORTZUGFERD_IMAP_PORT', '993');
        $imap_user = getDolGlobalString('IMPORTZUGFERD_IMAP_USER');
        $password = getDolGlobalString('IMPORTZUGFERD_IMAP_PASSWORD');
        $folder = getDolGlobalString('IMPORTZUGFERD_IMAP_FOLDER', 'INBOX');
        $ssl = getDolGlobalString('IMPORTZUGFERD_IMAP_SSL');
        $auto_create = getDolGlobalString('IMPORTZUGFERD_AUTO_CREATE_INVOICE');

        // Validate settings
        if (empty($host) || empty($imap_user) || empty($password)) {
            $this->error = 'IMAP settings not configured';
            $this->output = $this->error;
            return -1;
        }

        // Build mailbox string
        $mailbox = '{' . $host . ':' . $port . '/imap';
        if ($ssl) {
            $mailbox .= '/ssl';
        }
        $mailbox .= '/novalidate-cert}' . $folder;

        // Connect to IMAP
        $connection = @imap_open($mailbox, $imap_user, $password);

        if (!$connection) {
            $this->error = 'IMAP connection failed: ' . imap_last_error();
            $this->output = $this->error;
            return -2;
        }

        // Search for unread messages with attachments
        $messages = imap_search($connection, 'UNSEEN');

        if ($messages === false) {
            $this->output = 'No new messages found';
            imap_close($connection);
            return 0;
        }

        $temp_dir = $conf->importzugferd->dir_output . '/temp';
        if (!is_dir($temp_dir)) {
            dol_mkdir($temp_dir);
        }

        // Load admin user for import actions
        $admin_user = new User($this->db);
        $admin_user->fetch(1); // Fetch admin user

        $actions = new ActionsImportZugferd($this->db);

        foreach ($messages as $msg_num) {
            $structure = imap_fetchstructure($connection, $msg_num);

            // Check for attachments
            $attachments = $this->getAttachments($connection, $msg_num, $structure);

            foreach ($attachments as $attachment) {
                // Check if it's a PDF
                if (strtolower($attachment['type']) !== 'pdf') {
                    continue;
                }

                // Save attachment temporarily
                $temp_file = $temp_dir . '/' . uniqid('zugferd_') . '.pdf';
                file_put_contents($temp_file, $attachment['data']);

                // Check if it's a ZUGFeRD PDF
                $parser = new ZugferdParser($this->db);
                $result = $parser->extractFromPdf($temp_file);

                if ($result > 0) {
                    // It's a ZUGFeRD invoice, try to import
                    $result = $actions->processPdf($temp_file, $admin_user, $auto_create);

                    if ($result > 0) {
                        $this->imported_count++;
                        dol_syslog("CronImportZugferd: Imported invoice from email, ID: " . $result, LOG_INFO);
                    } elseif ($result == -3) {
                        // Duplicate
                        $this->skipped_count++;
                        dol_syslog("CronImportZugferd: Skipped duplicate invoice", LOG_INFO);
                    } else {
                        $this->error_count++;
                        $this->errors[] = $actions->error;
                        dol_syslog("CronImportZugferd: Error importing invoice: " . $actions->error, LOG_WARNING);
                    }
                }

                // Clean up temp file
                if (file_exists($temp_file)) {
                    unlink($temp_file);
                }
            }

            // Mark message as read
            imap_setflag_full($connection, (string)$msg_num, '\\Seen');
        }

        imap_close($connection);

        // Build output message
        $this->output = sprintf(
            "Processed %d messages. Imported: %d, Skipped (duplicates): %d, Errors: %d",
            count($messages),
            $this->imported_count,
            $this->skipped_count,
            $this->error_count
        );

        if ($this->error_count > 0) {
            $this->output .= "\nErrors: " . implode(", ", $this->errors);
        }

        return 0;
    }

    /**
     * Extract attachments from email
     *
     * @param resource $connection IMAP connection
     * @param int $msg_num Message number
     * @param object $structure Message structure
     * @param string $part_num Part number for nested parts
     * @return array Attachments
     */
    private function getAttachments($connection, $msg_num, $structure, $part_num = '')
    {
        $attachments = array();

        // Check if it's a multipart message
        if (isset($structure->parts) && count($structure->parts)) {
            foreach ($structure->parts as $key => $part) {
                $attachments = array_merge(
                    $attachments,
                    $this->getAttachments($connection, $msg_num, $part, ($part_num ? $part_num . '.' : '') . ($key + 1))
                );
            }
        } else {
            // Check if this part is an attachment
            $attachment = $this->extractAttachment($connection, $msg_num, $structure, $part_num);
            if ($attachment) {
                $attachments[] = $attachment;
            }
        }

        return $attachments;
    }

    /**
     * Extract a single attachment
     *
     * @param resource $connection IMAP connection
     * @param int $msg_num Message number
     * @param object $part Part structure
     * @param string $part_num Part number
     * @return array|null Attachment data or null
     */
    private function extractAttachment($connection, $msg_num, $part, $part_num)
    {
        $filename = '';

        // Get filename from parameters
        if (isset($part->dparameters)) {
            foreach ($part->dparameters as $param) {
                if (strtolower($param->attribute) === 'filename') {
                    $filename = $param->value;
                    break;
                }
            }
        }

        if (empty($filename) && isset($part->parameters)) {
            foreach ($part->parameters as $param) {
                if (strtolower($param->attribute) === 'name') {
                    $filename = $param->value;
                    break;
                }
            }
        }

        // Check if it's a PDF attachment
        if (empty($filename) || !preg_match('/\.pdf$/i', $filename)) {
            return null;
        }

        // Get attachment data
        if ($part_num) {
            $data = imap_fetchbody($connection, $msg_num, $part_num);
        } else {
            $data = imap_body($connection, $msg_num);
        }

        // Decode based on encoding
        if (isset($part->encoding)) {
            switch ($part->encoding) {
                case 3: // BASE64
                    $data = base64_decode($data);
                    break;
                case 4: // QUOTED-PRINTABLE
                    $data = quoted_printable_decode($data);
                    break;
            }
        }

        // Get file extension
        $ext = pathinfo($filename, PATHINFO_EXTENSION);

        return array(
            'filename' => $filename,
            'type' => strtolower($ext),
            'data' => $data
        );
    }

    /**
     * Move file to target folder with proper error handling
     *
     * @param string $file Source file path
     * @param string $targetFolder Target folder path
     * @param string $prefix Filename prefix (e.g., 'imported_', 'duplicate_', 'error_')
     * @return bool True if moved/deleted, false on failure
     */
    protected function moveFile($file, $targetFolder, $prefix = '')
    {
        if (!file_exists($file)) {
            dol_syslog("CronImportZugferd: File not found: " . $file, LOG_WARNING);
            return false;
        }

        // If target folder is configured and exists/writable
        if (!empty($targetFolder)) {
            // Create folder if it doesn't exist
            if (!is_dir($targetFolder)) {
                $result = dol_mkdir($targetFolder);
                if ($result < 0) {
                    dol_syslog("CronImportZugferd: Failed to create folder: " . $targetFolder, LOG_WARNING);
                }
            }

            if (is_dir($targetFolder) && is_writable($targetFolder)) {
                // Originalen Dateinamen beibehalten, bei Namenskollision Zaehler anhaengen
                $baseName = basename($file);
                $targetPath = $targetFolder . '/' . $baseName;
                if (file_exists($targetPath)) {
                    $pathInfo = pathinfo($baseName);
                    $counter = 1;
                    do {
                        $targetPath = $targetFolder . '/' . $pathInfo['filename'] . '_' . $counter . '.' . $pathInfo['extension'];
                        $counter++;
                    } while (file_exists($targetPath));
                }

                if (@rename($file, $targetPath)) {
                    dol_syslog("CronImportZugferd: Moved file to: " . $targetPath, LOG_INFO);
                    return true;
                } else {
                    // Try copy + delete as fallback (for cross-filesystem moves)
                    if (@copy($file, $targetPath)) {
                        @unlink($file);
                        dol_syslog("CronImportZugferd: Copied file to: " . $targetPath, LOG_INFO);
                        return true;
                    } else {
                        dol_syslog("CronImportZugferd: Failed to move/copy file to: " . $targetPath, LOG_ERR);
                        return false;
                    }
                }
            } else {
                dol_syslog("CronImportZugferd: Target folder not writable: " . $targetFolder, LOG_WARNING);
            }
        }

        // No target folder configured or not writable - delete file from watch folder
        // to prevent re-processing (except for errors without error folder)
        if ($prefix !== 'error_') {
            if (@unlink($file)) {
                dol_syslog("CronImportZugferd: Deleted processed file: " . $file, LOG_INFO);
                return true;
            } else {
                dol_syslog("CronImportZugferd: Failed to delete file: " . $file, LOG_ERR);
                return false;
            }
        }

        // Error files stay in watch folder if no error folder configured
        dol_syslog("CronImportZugferd: Keeping error file in watch folder: " . $file, LOG_INFO);
        return true;
    }

    /**
     * Send notifications based on import results
     *
     * @return void
     */
    protected function sendImportNotifications()
    {
        // Check for errors
        if ($this->error_count > 0) {
            $errorSummary = count($this->errors) > 0 ? implode(', ', array_slice($this->errors, 0, 3)) : 'Siehe Log';
            $this->notify(
                'warning',
                $this->error_count.' ZUGFeRD Import-Fehler',
                $errorSummary,
                dol_buildpath('/importzugferd/list.php?status=error', 1),
                'Fehler anzeigen'
            );
        }

        // Check for imported invoices that need review
        if ($this->imported_count > 0) {
            // Count pending invoices (drafts needing approval)
            $pendingCount = $this->countPendingInvoices();

            if ($pendingCount > 0) {
                $this->notify(
                    'action',
                    $this->imported_count.' ZUGFeRD Rechnungen importiert',
                    "{$pendingCount} Lieferantenrechnungen warten auf Prüfung und Freigabe",
                    dol_buildpath('/fourn/facture/list.php?search_status=0', 1),
                    'Rechnungen prüfen'
                );
            } else {
                // All auto-created and validated
                $this->notify(
                    'info',
                    $this->imported_count.' ZUGFeRD Rechnungen importiert',
                    'Alle Rechnungen wurden erfolgreich verarbeitet',
                    dol_buildpath('/fourn/facture/list.php', 1),
                    'Anzeigen'
                );
            }
        }

        // IMAP connection issues
        if (strpos($this->error, 'IMAP connection failed') !== false) {
            $this->notify(
                'error',
                'IMAP Verbindung fehlgeschlagen',
                'E-Mail Postfach für ZUGFeRD-Import nicht erreichbar',
                dol_buildpath('/importzugferd/admin/setup.php', 1),
                'IMAP prüfen'
            );
        }
    }

    /**
     * Count pending (draft) supplier invoices
     *
     * @return int Number of draft supplier invoices
     */
    protected function countPendingInvoices()
    {
        $sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_fourn";
        $sql .= " WHERE fk_statut = 0"; // Draft status
        $sql .= " AND entity IN (".getEntity('facture_fourn').")";

        $resql = $this->db->query($sql);
        if ($resql) {
            $obj = $this->db->fetch_object($resql);
            return (int) $obj->cnt;
        }
        return 0;
    }
}
