<?php

namespace objects;

use classes\db;
use classes\object_property;
use classes\xlvask;
use classes\xlvask_usage_logs_schema_bootstrap;
use Exception;
use helpers\xlvask_customer;
use helpers\xlvask_usage_log;
use helpers\xlvask_vehicle;
use traits\db_object_t;

class xlvask_usage_logs_o extends db
{
    use db_object_t;

    public object_property $WashId;
    public object_property $CustomerId;
    public object_property $Customer;
    public object_property $VatNumber;
    public object_property $Location;
    public object_property $Hall;
    public object_property $HallId;
    public object_property $StartTime;
    public object_property $FinishTime;
    public object_property $RegistrationNumber;
    public object_property $VehicleType;
    public object_property $IdentificationType;
    public object_property $IdentificationId;
    public object_property $Info;
    public object_property $Updated;
    public object_property $Prepaid;
    public object_property $FinishStatus;
    public object_property $CustomerGuid;
    public object_property $VehicleId;
    public object_property $WashItems;
    public object_property $ignored_at;
    public object_property $ignored_by;
    public object_property $ignored_reason;

    public function structure(): void
    {
        xlvask_usage_logs_schema_bootstrap::ensureTables();
        $this->setTable('xlvask_usage_logs');
    }

    /**
     * Add a new XL Vask customer
     * @param array $data The properties
     * @returns void
     * @throws Exception If the object was not created successfully
     */
    public function add(array $data): void
    {
        $tmp_id = self::add_object($data);
        $this->id = $tmp_id;
        self::getObjectProperties();
        self::objectChanged();
    }

    public function getObjectProperties(): void
    {
        $this->WashId = new object_property($this->table, $this->id, 'WashId', 'string', false);
        $this->CustomerId = new object_property($this->table, $this->id, 'CustomerId', 'string', false);
        $this->Customer = new object_property($this->table, $this->id, 'Customer', 'string', false);
        $this->VatNumber = new object_property($this->table, $this->id, 'VatNumber', 'string', false);
        $this->Location = new object_property($this->table, $this->id, 'Location', 'string', false);
        $this->Hall = new object_property($this->table, $this->id, 'Hall', 'string', false);
        $this->HallId = new object_property($this->table, $this->id, 'HallId', 'string', false);
        $this->StartTime = new object_property($this->table, $this->id, 'StartTime', 'string', false);
        $this->FinishTime = new object_property($this->table, $this->id, 'FinishTime', 'string', false);
        $this->RegistrationNumber = new object_property($this->table, $this->id, 'RegistrationNumber', 'string', false);
        $this->VehicleType = new object_property($this->table, $this->id, 'VehicleType', 'string', false);
        $this->IdentificationType = new object_property($this->table, $this->id, 'IdentificationType', 'string', false);
        $this->IdentificationId = new object_property($this->table, $this->id, 'IdentificationId', 'string', false);
        $this->Info = new object_property($this->table, $this->id, 'Info', 'string', false);
        $this->Updated = new object_property($this->table, $this->id, 'Updated', 'string', false);
        $this->Prepaid = new object_property($this->table, $this->id, 'Prepaid', 'string', false);
        $this->FinishStatus = new object_property($this->table, $this->id, 'FinishStatus', 'string', false);
        $this->CustomerGuid = new object_property($this->table, $this->id, 'CustomerGuid', 'string', false);
        $this->VehicleId = new object_property($this->table, $this->id, 'VehicleId', 'string', false);
        $this->WashItems = new object_property($this->table, $this->id, 'WashItems', 'string', false);
        $this->ignored_at = new object_property($this->table, $this->id, 'ignored_at', 'datetime', false);
        $this->ignored_by = new object_property($this->table, $this->id, 'ignored_by', 'int', false);
        $this->ignored_reason = new object_property($this->table, $this->id, 'ignored_reason', 'string', false);
    }

    public function objectChanged(): void
    {
        //TODO: Add cache invalidation
    }

    public function getCachedAmountSummaryFromRow(array $row): array
    {
        $cached_amount = self::normalizeMoneyValue($row['cached_total_net_amount'] ?? null);
        $cached_at = trim((string)($row['cached_amount_at'] ?? ''));

        if ($cached_amount !== null && $cached_at !== '') {
            return [
                'total_net_amount' => $cached_amount,
                'primary_product_name' => (string)($row['cached_primary_product_name'] ?? ''),
                'cached' => true,
            ];
        }

        $summary = self::calculateAmountSummaryFromWashItems($row['WashItems'] ?? []);
        $id = (int)($row['id'] ?? 0);
        if ($id > 0) {
            self::cacheAmountSummary($id, $summary);
        }

        return [
            ...$summary,
            'cached' => false,
        ];
    }

    public static function calculateAmountSummaryFromWashItems(array|string|null $washItems): array
    {
        if (is_string($washItems)) {
            $decoded = json_decode($washItems, true);
            $washItems = is_array($decoded) ? $decoded : [];
        }

        $total = 0.0;
        $primaryProductName = '';
        foreach (is_array($washItems) ? $washItems : [] as $item) {
            if (!is_array($item)) {
                continue;
            }

            if ($primaryProductName === '' && isset($item['OriginalProductName'])) {
                $primaryProductName = trim((string)$item['OriginalProductName']);
            }

            $priceIncVat = self::normalizeMoneyValue($item['PriceIncVat'] ?? null);
            $vat = self::normalizeMoneyValue($item['Vat'] ?? 0.0) ?? 0.0;
            if ($priceIncVat === null) {
                continue;
            }

            $total += $priceIncVat - $vat;
        }

        return [
            'total_net_amount' => round($total, 2),
            'primary_product_name' => $primaryProductName,
        ];
    }

    private static function cacheAmountSummary(int $id, array $summary): void
    {
        global $db;

        if (!isset($db) || !is_object($db) || !method_exists($db, 'query')) {
            return;
        }

        $amount = number_format((float)($summary['total_net_amount'] ?? 0.0), 2, '.', '');
        $primaryProductName = $db->escape_string((string)($summary['primary_product_name'] ?? ''));
        $db->query(
            "UPDATE xlvask_usage_logs
             SET cached_total_net_amount = {$amount},
                 cached_primary_product_name = " . ($primaryProductName === '' ? 'NULL' : "'{$primaryProductName}'") . ",
                 cached_amount_at = NOW()
             WHERE id = {$id}"
        );
    }

    private static function normalizeMoneyValue(mixed $value): ?float
    {
        if ($value === null || $value === '') {
            return null;
        }

        if (is_int($value) || is_float($value)) {
            return (float)$value;
        }

        $normalized = preg_replace('/[^\d,.\-]/', '', (string)$value);
        if ($normalized === null || $normalized === '') {
            return null;
        }

        if (str_contains($normalized, ',') && !str_contains($normalized, '.')) {
            $normalized = str_replace(',', '.', $normalized);
        } else {
            $normalized = str_replace(',', '', $normalized);
        }

        return is_numeric($normalized) ? (float)$normalized : null;
    }

    /**
     * Import the usage logs from XL Vask
     * @param string|null $dateFrom Optional import start date or date-time modifier. Defaults to '-7 days'.
     * @param string|null $dateTo Optional inclusive import end date.
     * @throws Exception If the objects were not successfully added.
     * @returns void
     */
    public function importUsageLogs(?string $dateFrom = null, ?string $dateTo = null): void
    {
        if (!empty($this->id)) {
            throw new Exception('To prevent issues, having a selected object is not allowed.');
        }
        $usage_logs = $this->getUsageLogsFromXLVask(
            self::formatImportDateFrom($dateFrom) // Example: '2025-05-01T00:00:00.000'
        );
        $usage_logs = self::filterUsageLogsUntil($usage_logs, $dateTo);
        /** @var string[] $known_usage_logIds The XL Vask usage logIds currently known */
        $known_usage_logIds = array_map(function ($log) {
            return $log['WashId'];
        },
            self::getFieldsWhere(
                ['WashId' => array_column($usage_logs, 'WashId')],
                ['WashId']
            ));
        /** The XL Vask usage logs without a matching WashId in the database */
        $new_usage_logs = array_filter($usage_logs, function ($log) use ($known_usage_logIds) {
            return !in_array($log->WashId, $known_usage_logIds);
        });
        unset($known_usage_logIds);
        unset($usage_logs);
        /** Adding the new usage logs */
        foreach ($new_usage_logs as $log) {
            if (!$log->isValid()) {
                echo $log->formattedDetails();
                throw new Exception('A usage log from XL Vask is not valid, have the structure changed?');
            }
        }
        // Actually save the usage log
        foreach ($new_usage_logs as $log) {
            /** @var xlvask_usage_log $log */
            $this->add($log->toArray());
        }
        unset($new_usage_logs);
    }

    private static function formatImportDateFrom(?string $dateFrom): string
    {
        $dateFrom = trim((string)($dateFrom ?? ''));
        $timestamp = strtotime($dateFrom === '' ? '-7 days' : $dateFrom);

        if ($timestamp === false) {
            throw new Exception('Invalid XL Vask usage import dateFrom');
        }

        return date('Y-m-d\TH:i:s.000', $timestamp);
    }

    /**
     * @param xlvask_usage_log[] $usageLogs
     * @return xlvask_usage_log[]
     * @throws Exception
     */
    private static function filterUsageLogsUntil(array $usageLogs, ?string $dateTo): array
    {
        $dateTo = trim((string)($dateTo ?? ''));
        if ($dateTo === '') {
            return $usageLogs;
        }

        $dateToTimestamp = strtotime($dateTo);
        if ($dateToTimestamp === false) {
            throw new Exception('Invalid XL Vask usage import dateTo');
        }

        $inclusiveEndTimestamp = strtotime(date('Y-m-d 23:59:59', $dateToTimestamp));
        if ($inclusiveEndTimestamp === false) {
            throw new Exception('Invalid XL Vask usage import dateTo');
        }

        return array_values(array_filter($usageLogs, function (xlvask_usage_log $log) use ($inclusiveEndTimestamp) {
            $startTimestamp = strtotime((string)$log->StartTime);
            return $startTimestamp !== false && $startTimestamp <= $inclusiveEndTimestamp;
        }));
    }

    /**
     * This function retrieves the usage logs from XL Vask
     * @param string $fromDate The date from which to retrieve the usage logs, in ISO 8601 format (e.g., '2025-05-01T00:00:00.000')
     * @returns xlvask_usage_log[]
     * @throws Exception
     */
    public function getUsageLogsFromXLVask(string $fromDate): array
    {
        // Validate the date format
        if (!preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$/', $fromDate)) {
            throw new Exception('Invalid date format. Expected format: YYYY-MM-DDTHH:MM:SS.SSS (e.g., 2025-05-01T00:00:00.000)');
        }
        // Set the memory limit to 512MB, as the import can be quite large
        ini_set('memory_limit', '512M');
        /** Get the vehicles from XL Vask */
        $xlvask = new xlvask();
        $vehicle_class = $xlvask->new($xlvask->helpers->xlvask_usage_log);
        /** @var xlvask_usage_log[] $vehicles */
        $vehicles = $vehicle_class->toObjects($xlvask->getUsageLog(
            $fromDate, // Example: '2025-05-01T00:00:00.000'
        ));
        return $vehicles;
    }
}
