<?php

declare(strict_types=1);

namespace App\Framework\Tracing\Exporters;

use App\Framework\Database\Connection;
use App\Framework\Logging\Logger;
use App\Framework\Tracing\TraceExporter;

/**
 * Database Trace Exporter
 *
 * Exports traces to database for persistence and analysis.
 * Supports efficient batch insertion and query optimization.
 */
final readonly class DatabaseTraceExporter implements TraceExporter
{
    public function __construct(
        private Connection $connection,
        private ?Logger $logger = null,
        private string $tracesTable = 'traces',
        private string $spansTable = 'trace_spans',
        private int $batchSize = 100
    ) {
    }

    public function export(array $traceData): void
    {
        try {
            $this->connection->beginTransaction();

            // Insert trace record
            $traceId = $this->insertTrace($traceData);

            // Insert spans
            $this->insertSpans($traceId, $traceData['spans'] ?? []);

            $this->connection->commit();

            $this->logger?->debug('Trace exported to database', [
                'trace_id' => $traceData['traceId'] ?? 'unknown',
                'database_id' => $traceId,
            ]);
        } catch (\Throwable $e) {
            $this->connection->rollBack();

            $this->logger?->error('Failed to export trace to database', [
                'error' => $e->getMessage(),
                'trace_id' => $traceData['traceId'] ?? 'unknown',
            ]);
        }
    }

    private function insertTrace(array $traceData): int
    {
        $sql = "
            INSERT INTO {$this->tracesTable} (
                trace_id,
                start_time,
                end_time,
                duration,
                span_count,
                error_count,
                status,
                tags,
                created_at
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
        ";

        $generator = new \App\Framework\Ulid\UlidGenerator();
        $traceId = $traceData['traceId'] ?? $generator->generate();
        $startTime = $traceData['startTime'] ?? microtime(true);
        $endTime = $traceData['endTime'] ?? ($startTime + ($traceData['duration'] ?? 0));
        $duration = $traceData['duration'] ?? 0;

        $spans = $traceData['spans'] ?? [];
        $spanCount = count($spans);
        $errorCount = count(array_filter($spans, fn ($span) => $span['hasError'] ?? false));
        $status = $errorCount > 0 ? 'error' : 'ok';

        $this->connection->execute($sql, [
            $traceId,
            date('Y-m-d H:i:s', (int)$startTime),
            date('Y-m-d H:i:s', (int)$endTime),
            $duration,
            $spanCount,
            $errorCount,
            $status,
            json_encode($traceData['tags'] ?? []),
        ]);

        return $this->connection->lastInsertId();
    }

    private function insertSpans(int $traceId, array $spans): void
    {
        if (empty($spans)) {
            return;
        }

        $sql = "
            INSERT INTO {$this->spansTable} (
                trace_id,
                span_id,
                parent_span_id,
                name,
                operation,
                start_time,
                end_time,
                duration,
                status,
                has_error,
                error_message,
                tags,
                events,
                created_at
            ) VALUES " . str_repeat('(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW()),', count($spans));

        // Remove trailing comma
        $sql = rtrim($sql, ',');

        $generator = new \App\Framework\Ulid\UlidGenerator();
        $values = [];
        foreach ($spans as $span) {
            $spanStartTime = $span['startTime'] ?? microtime(true);
            $spanDuration = $span['duration'] ?? 0;
            $spanEndTime = $spanStartTime + $spanDuration;

            $values = array_merge($values, [
                $traceId,
                $span['spanId'] ?? $generator->generate(),
                $span['parentSpanId'] ?? null,
                $span['name'] ?? 'unknown',
                $span['operation'] ?? 'unknown',
                date('Y-m-d H:i:s', (int)$spanStartTime),
                date('Y-m-d H:i:s', (int)$spanEndTime),
                $spanDuration,
                ($span['hasError'] ?? false) ? 'error' : 'ok',
                ($span['hasError'] ?? false) ? 1 : 0,
                $span['errorMessage'] ?? null,
                json_encode($span['tags'] ?? []),
                json_encode($span['events'] ?? []),
            ]);
        }

        $this->connection->execute($sql, $values);
    }

    public function createTables(): void
    {
        $tracesSql = "
            CREATE TABLE IF NOT EXISTS {$this->tracesTable} (
                id INT AUTO_INCREMENT PRIMARY KEY,
                trace_id VARCHAR(64) NOT NULL UNIQUE,
                start_time DATETIME NOT NULL,
                end_time DATETIME NOT NULL,
                duration DECIMAL(10,6) NOT NULL,
                span_count INT NOT NULL DEFAULT 0,
                error_count INT NOT NULL DEFAULT 0,
                status ENUM('ok', 'error') NOT NULL DEFAULT 'ok',
                tags JSON,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_trace_id (trace_id),
                INDEX idx_start_time (start_time),
                INDEX idx_status (status),
                INDEX idx_duration (duration)
            )
        ";

        $spansSql = "
            CREATE TABLE IF NOT EXISTS {$this->spansTable} (
                id INT AUTO_INCREMENT PRIMARY KEY,
                trace_id INT NOT NULL,
                span_id VARCHAR(64) NOT NULL,
                parent_span_id VARCHAR(64) NULL,
                name VARCHAR(255) NOT NULL,
                operation VARCHAR(255) NOT NULL,
                start_time DATETIME NOT NULL,
                end_time DATETIME NOT NULL,
                duration DECIMAL(10,6) NOT NULL,
                status ENUM('ok', 'error') NOT NULL DEFAULT 'ok',
                has_error BOOLEAN NOT NULL DEFAULT FALSE,
                error_message TEXT NULL,
                tags JSON,
                events JSON,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (trace_id) REFERENCES {$this->tracesTable}(id) ON DELETE CASCADE,
                INDEX idx_trace_id (trace_id),
                INDEX idx_span_id (span_id),
                INDEX idx_parent_span_id (parent_span_id),
                INDEX idx_name (name),
                INDEX idx_start_time (start_time),
                INDEX idx_duration (duration),
                INDEX idx_has_error (has_error)
            )
        ";

        $this->connection->execute($tracesSql);
        $this->connection->execute($spansSql);
    }

    public function getTraceById(string $traceId): ?array
    {
        $traceSql = "SELECT * FROM {$this->tracesTable} WHERE trace_id = ?";
        $trace = $this->connection->fetchOne($traceSql, [$traceId]);

        if (! $trace) {
            return null;
        }

        $spansSql = "
            SELECT * FROM {$this->spansTable} 
            WHERE trace_id = ? 
            ORDER BY start_time ASC
        ";
        $spans = $this->connection->fetchAll($spansSql, [$trace['id']]);

        return [
            'traceId' => $trace['trace_id'],
            'startTime' => strtotime($trace['start_time']),
            'endTime' => strtotime($trace['end_time']),
            'duration' => (float)$trace['duration'],
            'spanCount' => $trace['span_count'],
            'errorCount' => $trace['error_count'],
            'status' => $trace['status'],
            'tags' => json_decode($trace['tags'] ?? '[]', true),
            'spans' => array_map(function ($span) {
                return [
                    'spanId' => $span['span_id'],
                    'parentSpanId' => $span['parent_span_id'],
                    'name' => $span['name'],
                    'operation' => $span['operation'],
                    'startTime' => strtotime($span['start_time']),
                    'endTime' => strtotime($span['end_time']),
                    'duration' => (float)$span['duration'],
                    'status' => $span['status'],
                    'hasError' => (bool)$span['has_error'],
                    'errorMessage' => $span['error_message'],
                    'tags' => json_decode($span['tags'] ?? '[]', true),
                    'events' => json_decode($span['events'] ?? '[]', true),
                ];
            }, $spans),
        ];
    }

    public function searchTraces(array $criteria = [], int $limit = 100, int $offset = 0): array
    {
        $conditions = [];
        $params = [];

        if (! empty($criteria['start_time_from'])) {
            $conditions[] = 'start_time >= ?';
            $params[] = $criteria['start_time_from'];
        }

        if (! empty($criteria['start_time_to'])) {
            $conditions[] = 'start_time <= ?';
            $params[] = $criteria['start_time_to'];
        }

        if (! empty($criteria['status'])) {
            $conditions[] = 'status = ?';
            $params[] = $criteria['status'];
        }

        if (! empty($criteria['min_duration'])) {
            $conditions[] = 'duration >= ?';
            $params[] = $criteria['min_duration'];
        }

        if (! empty($criteria['max_duration'])) {
            $conditions[] = 'duration <= ?';
            $params[] = $criteria['max_duration'];
        }

        $whereClause = ! empty($conditions) ? 'WHERE ' . implode(' AND ', $conditions) : '';

        $sql = "
            SELECT trace_id, start_time, end_time, duration, span_count, error_count, status
            FROM {$this->tracesTable} 
            {$whereClause}
            ORDER BY start_time DESC 
            LIMIT ? OFFSET ?
        ";

        $params[] = $limit;
        $params[] = $offset;

        return $this->connection->fetchAll($sql, $params);
    }
}
