<?php

namespace MintyPHP\Repository\Audit;

use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\SystemAuditChannel;
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
use MintyPHP\Repository\Support\RepoQuery;

class SystemAuditLogRepository implements SystemAuditLogRepositoryInterface
{
    private const FILTER_OPTIONS_LIMIT_MAX = 200;

    public function create(array $data): int|false
    {
        $id = DB::insert(
            'insert into system_audit_log (
                event_uuid, request_id, channel, event_type, outcome, error_code,
                actor_user_id, actor_tenant_id, target_type, target_id, target_uuid,
                method, path, ip_hash, user_agent_hash, metadata_json, created_at
            ) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())',
            (string) ($data['event_uuid'] ?? ''),
            $data['request_id'] ?? null,
            (string) ($data['channel'] ?? ''),
            (string) ($data['event_type'] ?? ''),
            (string) ($data['outcome'] ?? SystemAuditOutcome::Success->value),
            $data['error_code'] ?? null,
            $data['actor_user_id'] !== null ? (string) ((int) $data['actor_user_id']) : null,
            $data['actor_tenant_id'] !== null ? (string) ((int) $data['actor_tenant_id']) : null,
            $data['target_type'] ?? null,
            $data['target_id'] !== null ? (string) ((int) $data['target_id']) : null,
            $data['target_uuid'] ?? null,
            $data['method'] ?? null,
            $data['path'] ?? null,
            $data['ip_hash'] ?? null,
            $data['user_agent_hash'] ?? null,
            $data['metadata_json'] ?? null
        );

        return $id ? (int) $id : false;
    }

    public function listPaged(array $filters): array
    {
        $search = trim((string) ($filters['search'] ?? ''));
        $eventTypes = RepoQuery::normalizeStringList(
            $filters['event_types'] ?? '',
            self::FILTER_OPTIONS_LIMIT_MAX,
            static function (string $value): string {
                $value = strtolower(trim($value));
                if ($value === '' || strlen($value) > 64) {
                    return '';
                }
                return preg_match('/^[a-z0-9._-]+$/', $value) === 1 ? $value : '';
            }
        );
        $outcome = strtolower(trim((string) ($filters['outcome'] ?? '')));
        $channel = strtolower(trim((string) ($filters['channel'] ?? '')));
        $createdFrom = trim((string) ($filters['created_from'] ?? ''));
        $createdTo = trim((string) ($filters['created_to'] ?? ''));
        $actorUserIds = array_slice(
            RepoQuery::normalizeIdList($filters['actor_user_ids'] ?? ''),
            0,
            self::FILTER_OPTIONS_LIMIT_MAX
        );
        $targetType = trim((string) ($filters['target_type'] ?? ''));
        $requestId = trim((string) ($filters['request_id'] ?? ''));

        [$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 200, 0);
        [$order, $dir] = RepoQuery::sanitizeOrder(
            $filters,
            ['id', 'created_at', 'event_type', 'outcome', 'channel', 'actor_user_id'],
            'created_at',
            'desc'
        );

        $where = [];
        $params = [];

        RepoQuery::addLikeFilter(
            $where,
            $params,
            ['system_audit_log.event_type', 'system_audit_log.path', 'system_audit_log.error_code', 'system_audit_log.request_id'],
            $search
        );

        if ($eventTypes !== []) {
            $where[] = 'system_audit_log.event_type in (???)';
            $params[] = $eventTypes;
        }

        $normalizedOutcome = SystemAuditOutcome::tryNormalize($outcome);
        if ($normalizedOutcome !== null) {
            $where[] = 'system_audit_log.outcome = ?';
            $params[] = $normalizedOutcome->value;
        }

        $normalizedChannel = SystemAuditChannel::tryNormalize($channel);
        if ($normalizedChannel !== null) {
            $where[] = 'system_audit_log.channel = ?';
            $params[] = $normalizedChannel->value;
        }

        if ($actorUserIds !== []) {
            $where[] = 'system_audit_log.actor_user_id in (???)';
            $params[] = $actorUserIds;
        }

        if ($targetType !== '') {
            $where[] = 'system_audit_log.target_type = ?';
            $params[] = $targetType;
        }

        if ($requestId !== '') {
            if (preg_match('/^[a-f0-9-]{36}$/i', $requestId)) {
                $where[] = 'system_audit_log.request_id = ?';
                $params[] = strtolower($requestId);
            } else {
                RepoQuery::addLikeFilter($where, $params, ['system_audit_log.request_id'], $requestId);
            }
        }

        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdFrom)) {
            $where[] = 'system_audit_log.created_at >= ?';
            $params[] = $createdFrom . ' 00:00:00';
        }
        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdTo)) {
            $where[] = 'system_audit_log.created_at <= ?';
            $params[] = $createdTo . ' 23:59:59';
        }

        $whereSql = $where ? (' where ' . implode(' and ', $where)) : '';
        $fromSql = ' from system_audit_log ' .
            'left join users actor_user on actor_user.id = system_audit_log.actor_user_id ' .
            'left join tenants actor_tenant on actor_tenant.id = system_audit_log.actor_tenant_id ';

        $total = (int) (DB::selectValue('select count(*)' . $fromSql . $whereSql, ...$params) ?? 0);

        $rows = DB::select(
            'select
                system_audit_log.id,
                system_audit_log.event_uuid,
                system_audit_log.request_id,
                system_audit_log.channel,
                system_audit_log.event_type,
                system_audit_log.outcome,
                system_audit_log.error_code,
                system_audit_log.actor_user_id,
                system_audit_log.actor_tenant_id,
                system_audit_log.target_type,
                system_audit_log.target_id,
                system_audit_log.target_uuid,
                system_audit_log.method,
                system_audit_log.path,
                system_audit_log.ip_hash,
                system_audit_log.user_agent_hash,
                system_audit_log.metadata_json,
                system_audit_log.created_at,
                actor_user.id,
                actor_user.uuid,
                actor_user.display_name,
                actor_user.email,
                actor_tenant.id,
                actor_tenant.uuid,
                actor_tenant.description
            ' . $fromSql . $whereSql .
                sprintf(' order by system_audit_log.`%s` %s limit ? offset ?', $order, $dir),
            ...array_merge($params, [(string) $limit, (string) $offset])
        );

        $normalized = [];
        if (is_array($rows)) {
            foreach ($rows as $row) {
                $item = $this->normalizeRow($row);
                if ($item !== null) {
                    $normalized[] = $item;
                }
            }
        }

        return [
            'total' => $total,
            'rows' => $normalized,
        ];
    }

    public function listFilterOptions(int $limit = self::FILTER_OPTIONS_LIMIT_MAX): array
    {
        $limit = max(1, min(self::FILTER_OPTIONS_LIMIT_MAX, $limit));

        $eventRows = DB::select(
            'select
                system_audit_log.event_type,
                max(system_audit_log.created_at) as last_used_at
            from system_audit_log
            where system_audit_log.event_type is not null
              and system_audit_log.event_type <> \'\'
            group by system_audit_log.event_type
            order by last_used_at desc
            limit ?',
            (string) $limit
        );

        $eventTypes = [];
        if (is_array($eventRows)) {
            foreach ($eventRows as $row) {
                $flat = self::flattenRow($row);
                $eventType = strtolower(trim((string) ($flat['event_type'] ?? '')));
                if ($eventType === '' || strlen($eventType) > 64) {
                    continue;
                }
                if (preg_match('/^[a-z0-9._-]+$/', $eventType) !== 1) {
                    continue;
                }
                $eventTypes[] = $eventType;
            }
        }
        $eventTypes = array_values(array_unique($eventTypes));

        $actorRows = DB::select(
            'select
                system_audit_log.actor_user_id,
                max(system_audit_log.created_at) as last_used_at,
                max(actor_user.id) as actor_user_exists_id,
                max(actor_user.display_name) as actor_user_display_name,
                max(actor_user.email) as actor_user_email
            from system_audit_log
            left join users actor_user on actor_user.id = system_audit_log.actor_user_id
            where system_audit_log.actor_user_id is not null
              and system_audit_log.actor_user_id > 0
            group by system_audit_log.actor_user_id
            order by last_used_at desc
            limit ?',
            (string) $limit
        );

        $actors = [];
        if (is_array($actorRows)) {
            foreach ($actorRows as $row) {
                $flat = self::flattenRow($row);
                $id = (int) ($flat['actor_user_id'] ?? 0);
                if ($id <= 0) {
                    continue;
                }
                $actors[] = [
                    'id' => $id,
                    'display_name' => trim((string) ($flat['actor_user_display_name'] ?? '')),
                    'email' => trim((string) ($flat['actor_user_email'] ?? '')),
                    'exists' => (int) ($flat['actor_user_exists_id'] ?? 0) > 0,
                ];
            }
        }

        return [
            'event_types' => $eventTypes,
            'actors' => $actors,
        ];
    }

    public function find(int $id): ?array
    {
        $row = DB::selectOne(
            'select
                system_audit_log.id,
                system_audit_log.event_uuid,
                system_audit_log.request_id,
                system_audit_log.channel,
                system_audit_log.event_type,
                system_audit_log.outcome,
                system_audit_log.error_code,
                system_audit_log.actor_user_id,
                system_audit_log.actor_tenant_id,
                system_audit_log.target_type,
                system_audit_log.target_id,
                system_audit_log.target_uuid,
                system_audit_log.method,
                system_audit_log.path,
                system_audit_log.ip_hash,
                system_audit_log.user_agent_hash,
                system_audit_log.metadata_json,
                system_audit_log.created_at,
                actor_user.id,
                actor_user.uuid,
                actor_user.display_name,
                actor_user.email,
                actor_tenant.id,
                actor_tenant.uuid,
                actor_tenant.description
            from system_audit_log
            left join users actor_user on actor_user.id = system_audit_log.actor_user_id
            left join tenants actor_tenant on actor_tenant.id = system_audit_log.actor_tenant_id
            where system_audit_log.id = ?
            limit 1',
            (string) $id
        );

        return $this->normalizeRow($row);
    }

    public function purgeOlderThanDays(int $days): int
    {
        if ($days <= 0) {
            return 0;
        }

        $cutoff = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))
            ->modify('-' . $days . ' days')
            ->format('Y-m-d H:i:s');

        $deleted = DB::delete('delete from system_audit_log where created_at < ?', $cutoff);
        return is_int($deleted) ? $deleted : 0;
    }

    private function normalizeRow(mixed $row): ?array
    {
        if (!is_array($row)) {
            return null;
        }

        $log = $row['system_audit_log'] ?? [];
        if (!is_array($log) || !isset($log['id'])) {
            return null;
        }

        $user = is_array($row['actor_user'] ?? null) ? $row['actor_user'] : [];
        $tenant = is_array($row['actor_tenant'] ?? null) ? $row['actor_tenant'] : [];

        $log['actor_user_uuid'] = (string) ($user['uuid'] ?? '');
        $log['actor_user_display_name'] = (string) ($user['display_name'] ?? '');
        $log['actor_user_email'] = (string) ($user['email'] ?? '');
        $log['actor_tenant_uuid'] = (string) ($tenant['uuid'] ?? '');
        $log['actor_tenant_description'] = (string) ($tenant['description'] ?? '');

        return $log;
    }

    /**
     * @return array<string, mixed>
     */
    private static function flattenRow(mixed $row): array
    {
        if (!is_array($row)) {
            return [];
        }

        $flat = [];
        foreach ($row as $key => $value) {
            if (is_array($value)) {
                $flat = array_merge($flat, $value);
            } elseif (is_string($key)) {
                $flat[$key] = $value;
            }
        }

        return $flat;
    }
}
