<?php

namespace MintyPHP\Repository\Audit;

use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;

class ApiAuditLogRepository implements ApiAuditLogRepositoryInterface
{
    private const FILTER_OPTIONS_LIMIT_MAX = 200;

    public function create(array $data): int|false
    {
        $id = DB::insert(
            'insert into api_audit_log (
                request_id, method, path, query_json, status_code, duration_ms, error_code,
                user_id, tenant_id, api_token_id, token_tenant_id, ip, user_agent, created_at
            ) values (?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())',
            (string) ($data['request_id'] ?? ''),
            (string) ($data['method'] ?? ''),
            (string) ($data['path'] ?? ''),
            $data['query_json'] ?? null,
            (string) ((int) ($data['status_code'] ?? 0)),
            $data['duration_ms'] !== null ? (string) ((int) $data['duration_ms']) : null,
            $data['error_code'] ?? null,
            $data['user_id'] !== null ? (string) ((int) $data['user_id']) : null,
            $data['tenant_id'] !== null ? (string) ((int) $data['tenant_id']) : null,
            $data['api_token_id'] !== null ? (string) ((int) $data['api_token_id']) : null,
            $data['token_tenant_id'] !== null ? (string) ((int) $data['token_tenant_id']) : null,
            $data['ip'] ?? null,
            $data['user_agent'] ?? null
        );

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

    public function listPaged(array $filters): array
    {
        $search = trim((string) ($filters['search'] ?? ''));
        $status = strtolower(trim((string) ($filters['status'] ?? '')));
        $method = strtoupper(trim((string) ($filters['method'] ?? '')));
        $createdFrom = trim((string) ($filters['created_from'] ?? ''));
        $createdTo = trim((string) ($filters['created_to'] ?? ''));
        $tenantIds = array_slice(
            RepoQuery::normalizeIdList($filters['tenant_ids'] ?? ''),
            0,
            self::FILTER_OPTIONS_LIMIT_MAX
        );
        $userIds = array_slice(
            RepoQuery::normalizeIdList($filters['user_ids'] ?? ''),
            0,
            self::FILTER_OPTIONS_LIMIT_MAX
        );

        [$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 200, 0);
        [$order, $dir] = RepoQuery::sanitizeOrder(
            $filters,
            ['id', 'created_at', 'status_code', 'duration_ms', 'method', 'path'],
            'created_at',
            'desc'
        );

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

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

        if (in_array($status, ['2xx', '4xx', '5xx'], true)) {
            $rangeStart = (int) $status[0] * 100;
            $where[] = 'api_audit_log.status_code between ? and ?';
            $params[] = (string) $rangeStart;
            $params[] = (string) ($rangeStart + 99);
        } elseif ($status !== '' && ctype_digit($status)) {
            $statusCode = (int) $status;
            if ($statusCode >= 100 && $statusCode <= 599) {
                $where[] = 'api_audit_log.status_code = ?';
                $params[] = (string) $statusCode;
            }
        }

        if (in_array($method, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], true)) {
            $where[] = 'api_audit_log.method = ?';
            $params[] = $method;
        }

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

        $whereSql = $where ? (' where ' . implode(' and ', $where)) : '';
        $fromSql = ' from api_audit_log ' .
            'left join users on users.id = api_audit_log.user_id ' .
            'left join tenants on tenants.id = api_audit_log.tenant_id ';

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

        $rows = DB::select(
            'select
                api_audit_log.id,
                api_audit_log.request_id,
                api_audit_log.method,
                api_audit_log.path,
                api_audit_log.query_json,
                api_audit_log.status_code,
                api_audit_log.duration_ms,
                api_audit_log.error_code,
                api_audit_log.user_id,
                api_audit_log.tenant_id,
                api_audit_log.api_token_id,
                api_audit_log.token_tenant_id,
                api_audit_log.ip,
                api_audit_log.user_agent,
                api_audit_log.created_at,
                users.id,
                users.uuid,
                users.display_name,
                users.email,
                tenants.id,
                tenants.uuid,
                tenants.description
            ' . $fromSql . $whereSql .
                sprintf(' order by api_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));

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

        $methods = [];
        if (is_array($methodRows)) {
            foreach ($methodRows as $row) {
                $flat = self::flattenRow($row);
                $method = strtoupper(trim((string) ($flat['method'] ?? '')));
                if ($method === '') {
                    continue;
                }
                $methods[] = $method;
            }
        }
        $methods = array_values(array_unique($methods));

        $userRows = DB::select(
            'select
                api_audit_log.user_id,
                max(api_audit_log.created_at) as last_used_at,
                max(users.id) as user_exists_id,
                max(users.display_name) as user_display_name,
                max(users.email) as user_email
            from api_audit_log
            left join users on users.id = api_audit_log.user_id
            where api_audit_log.user_id is not null
              and api_audit_log.user_id > 0
            group by api_audit_log.user_id
            order by last_used_at desc
            limit ?',
            (string) $limit
        );

        $users = [];
        if (is_array($userRows)) {
            foreach ($userRows as $row) {
                $flat = self::flattenRow($row);
                $id = (int) ($flat['user_id'] ?? 0);
                if ($id <= 0) {
                    continue;
                }
                $users[] = [
                    'id' => $id,
                    'display_name' => trim((string) ($flat['user_display_name'] ?? '')),
                    'email' => trim((string) ($flat['user_email'] ?? '')),
                    'exists' => (int) ($flat['user_exists_id'] ?? 0) > 0,
                ];
            }
        }

        $tenantRows = DB::select(
            'select
                api_audit_log.tenant_id,
                max(api_audit_log.created_at) as last_used_at,
                max(tenants.id) as tenant_exists_id,
                max(tenants.description) as tenant_description
            from api_audit_log
            left join tenants on tenants.id = api_audit_log.tenant_id
            where api_audit_log.tenant_id is not null
              and api_audit_log.tenant_id > 0
            group by api_audit_log.tenant_id
            order by last_used_at desc
            limit ?',
            (string) $limit
        );

        $tenants = [];
        if (is_array($tenantRows)) {
            foreach ($tenantRows as $row) {
                $flat = self::flattenRow($row);
                $id = (int) ($flat['tenant_id'] ?? 0);
                if ($id <= 0) {
                    continue;
                }
                $tenants[] = [
                    'id' => $id,
                    'description' => trim((string) ($flat['tenant_description'] ?? '')),
                    'exists' => (int) ($flat['tenant_exists_id'] ?? 0) > 0,
                ];
            }
        }

        return [
            'methods' => $methods,
            'users' => $users,
            'tenants' => $tenants,
        ];
    }

    public function find(int $id): ?array
    {
        $row = DB::selectOne(
            'select
                api_audit_log.id,
                api_audit_log.request_id,
                api_audit_log.method,
                api_audit_log.path,
                api_audit_log.query_json,
                api_audit_log.status_code,
                api_audit_log.duration_ms,
                api_audit_log.error_code,
                api_audit_log.user_id,
                api_audit_log.tenant_id,
                api_audit_log.api_token_id,
                api_audit_log.token_tenant_id,
                api_audit_log.ip,
                api_audit_log.user_agent,
                api_audit_log.created_at,
                users.id,
                users.uuid,
                users.display_name,
                users.email,
                tenants.id,
                tenants.uuid,
                tenants.description
            from api_audit_log
            left join users on users.id = api_audit_log.user_id
            left join tenants on tenants.id = api_audit_log.tenant_id
            where api_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 api_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['api_audit_log'] ?? [];
        if (!is_array($log) || !isset($log['id'])) {
            return null;
        }

        $user = is_array($row['users'] ?? null) ? $row['users'] : [];
        $tenant = is_array($row['tenants'] ?? null) ? $row['tenants'] : [];

        $log['user_uuid'] = (string) ($user['uuid'] ?? '');
        $log['user_display_name'] = (string) ($user['display_name'] ?? '');
        $log['user_email'] = (string) ($user['email'] ?? '');
        $log['tenant_uuid'] = (string) ($tenant['uuid'] ?? '');
        $log['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;
    }
}
