<?php

namespace App\Services;

use App\Libraries\ValueSet;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\HTTP\IncomingRequest;
use Config\Services;
use DateTime;
use DateTimeZone;

class AuditService
{
    private const TABLE_MAP = [
        'logpatient' => 'logpatient',
        'patient'    => 'logpatient',
        'visit'      => 'logpatient',
        'logorder'   => 'logorder',
        'order'      => 'logorder',
        'specimen'   => 'logorder',
        'result'     => 'logorder',
        'logmaster'  => 'logmaster',
        'master'     => 'logmaster',
        'config'     => 'logmaster',
        'valueset'   => 'logmaster',
        'logsystem'  => 'logsystem',
        'system'     => 'logsystem',
        'auth'       => 'logsystem',
        'job'        => 'logsystem',
    ];

    private const DEFAULT_APP_ID = 'clqms-api';
    private const ENTITY_VERSION_DEFAULT = 1;

    private static ?BaseConnection $db = null;
    private static $session = null;
    private static ?IncomingRequest $request = null;
    private static ?array $eventIdCache = null;
    private static ?string $cachedRequestId = null;

    public static function logData(
        string $eventId,
        string $activityId,
        string $entityType,
        string $entityId,
        string $tableName,
        ?string $fldName = null,
        $previousValue = null,
        $newValue = null,
        ?string $reason = null,
        ?array $context = null,
        array $options = []
    ): void {
        $sourceTable = $tableName ?: $entityType;
        $logTable = self::resolveLogTable($sourceTable);
        if ($logTable === null) {
            log_message('warning', "AuditService cannot resolve log table for {$sourceTable}");
            return;
        }

        $record = self::buildRecord(
            $logTable,
            self::normalizeEventId($eventId),
            strtoupper($activityId),
            $entityType,
            $entityId,
            $sourceTable,
            $fldName,
            $previousValue,
            $newValue,
            $reason,
            $context,
            $options
        );

        if ($record === null) {
            return;
        }

        try {
            self::getDb()->table($logTable)->insert($record);
        } catch (\Throwable $e) {
            log_message('error', "AuditService failed to insert into {$logTable}: {$e->getMessage()}");
        }
    }

    private static function buildRecord(
        string $logTable,
        string $eventId,
        string $activityId,
        string $entityType,
        string $entityId,
        string $tblName,
        ?string $fldName,
        $previousValue,
        $newValue,
        ?string $reason,
        ?array $context,
        array $options
    ): ?array {
        $contextJson = self::buildContext($context, $options, $entityType);
        if ($contextJson === null) {
            return null;
        }

        return [
            'TblName' => $tblName,
            'RecID' => (string) $entityId,
            'FldName' => $fldName,
            'FldValuePrev' => self::serializeValue($previousValue),
            'FldValueNew' => self::serializeValue($newValue),
            'UserID' => self::resolveUserId($options),
            'SiteID' => self::resolveSiteId($options),
            'DIDType' => $options['did_type'] ?? null,
            'DID' => $options['did'] ?? null,
            'MachineID' => $options['machine_id'] ?? gethostname(),
            'SessionID' => self::resolveSessionId($options),
            'AppID' => $options['app_id'] ?? self::DEFAULT_APP_ID,
            'ProcessID' => $options['process_id'] ?? null,
            'WebPageID' => $options['web_page_id'] ?? self::resolveRoute($options),
            'EventID' => $eventId,
            'ActivityID' => $activityId,
            'Reason' => $reason,
            'LogDate' => self::nowWithMillis(),
            'Context' => $contextJson,
            'IpAddress' => self::resolveIpAddress(),
        ];
    }

    private static function serializeValue($value): ?string
    {
        if ($value === null) {
            return null;
        }

        if (is_scalar($value)) {
            return (string) $value;
        }

        $json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return $json !== false ? $json : null;
    }

    private static function buildContext(?array $context, array $options, string $entityType): ?string
    {
        $route = $options['route'] ?? self::resolveRoute($options);
        $payload = array_merge(
            [
                'request_id' => $options['request_id'] ?? self::resolveRequestId(),
                'route' => $route,
                'timestamp_utc' => $options['timestamp_utc'] ?? self::timestampUtc(),
                'entity_type' => $options['entity_type'] ?? $entityType,
                'entity_version' => $options['entity_version'] ?? self::ENTITY_VERSION_DEFAULT,
            ],
            $context ?? []
        );

        $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return $json !== false ? $json : null;
    }

    private static function resolveLogTable(?string $source): ?string
    {
        if ($source === null) {
            return null;
        }

        $key = strtolower(trim($source));
        return self::TABLE_MAP[$key] ?? null;
    }

    private static function resolveUserId(array $options): string
    {
        return $options['user_id'] ?? self::getSessionValue('user_id') ?? 'SYSTEM';
    }

    private static function resolveSiteId(array $options): string
    {
        return $options['site_id'] ?? self::getSessionValue('site_id') ?? 'GLOBAL';
    }

    private static function resolveSessionId(array $options): string
    {
        if (!empty($options['session_id'])) {
            return $options['session_id'];
        }

        $session = self::getSession();
        if ($session !== null && method_exists($session, 'getId')) {
            $id = $session->getId();
            if (!empty($id)) {
                return $id;
            }
        }

        if (session_status() === PHP_SESSION_ACTIVE) {
            $id = session_id();
            if (!empty($id)) {
                return $id;
            }
        }

        return self::generateUniqueId('sess');
    }

    private static function resolveRoute(array $options): string
    {
        if (!empty($options['route'])) {
            return $options['route'];
        }

        $request = self::getRequest();
        if ($request !== null) {
            return trim(sprintf('%s %s', $request->getMethod(), $request->getUri()->getPath()));
        }

        return 'cli';
    }

    private static function resolveIpAddress(): ?string
    {
        $request = self::getRequest();
        if ($request !== null) {
            return $request->getIPAddress();
        }

        return $_SERVER['REMOTE_ADDR'] ?? null;
    }

    private static function normalizeEventId(string $eventId): string
    {
        $normalized = strtoupper(trim($eventId));
        if (empty($normalized)) {
            log_message('warning', 'AuditService received empty EventID');
            return $eventId;
        }

        if (!self::isKnownEvent($normalized)) {
            log_message('warning', "AuditService unknown EventID: {$normalized}");
        }

        return $normalized;
    }

    private static function isKnownEvent(string $eventId): bool
    {
        if (self::$eventIdCache === null) {
            $raw = ValueSet::getRaw('event_id') ?? [];
            self::$eventIdCache = array_filter(array_map(fn ($item) => $item['key'] ?? null, $raw));
        }

        return in_array($eventId, self::$eventIdCache, true);
    }

    private static function resolveRequestId(): string
    {
        if (self::$cachedRequestId !== null) {
            return self::$cachedRequestId;
        }

        $request = self::getRequest();
        if ($request !== null) {
            $value = $request->getHeaderLine('X-Request-ID');
            if (!empty($value)) {
                self::$cachedRequestId = $value;
                return $value;
            }
        }

        foreach (['HTTP_X_REQUEST_ID', 'REQUEST_ID'] as $header) {
            if (!empty($_SERVER[$header])) {
                self::$cachedRequestId = $_SERVER[$header];
                return self::$cachedRequestId;
            }
        }

        return self::$cachedRequestId = self::generateUniqueId('req');
    }

    private static function nowWithMillis(): string
    {
        $dt = new DateTime('now', new DateTimeZone('UTC'));
        return $dt->format('Y-m-d H:i:s.v');
    }

    private static function timestampUtc(): string
    {
        $dt = new DateTime('now', new DateTimeZone('UTC'));
        return $dt->format('Y-m-d\TH:i:s.v\Z');
    }

    private static function getSessionValue(string $key): ?string
    {
        $session = self::getSession();
        if ($session === null) {
            return null;
        }

        if (!method_exists($session, 'get')) {
            return null;
        }

        $value = $session->get($key);
        return $value !== null ? (string) $value : null;
    }

    private static function getSession(): ?object
    {
        if (self::$session !== null) {
            return self::$session;
        }

        try {
            return self::$session = Services::session();
        } catch (\Throwable $e) {
            return self::$session = null;
        }
    }

    private static function getRequest(): ?IncomingRequest
    {
        if (self::$request !== null) {
            return self::$request;
        }

        try {
            return self::$request = Services::request();
        } catch (\Throwable $e) {
            return self::$request = null;
        }
    }

    private static function getDb(): BaseConnection
    {
        return self::$db ??= \Config\Database::connect();
    }

    private static function generateUniqueId(string $prefix): string
    {
        try {
            return $prefix . '_' . bin2hex(random_bytes(8));
        } catch (\Throwable $e) {
            return uniqid("{$prefix}_", true);
        }
    }
}
