<?php

declare(strict_types=1);

namespace App\Framework\Telemetry;

use App\Framework\Attributes\Singleton;
use App\Framework\CircuitBreaker\CircuitBreaker;
use App\Framework\DateTime\Clock;
use App\Framework\Logging\Logger;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Telemetry\Config\TelemetryConfig;
use App\Framework\Telemetry\Exporters\TelemetryExporterInterface;
use App\Framework\Telemetry\ValueObjects\Event;
use App\Framework\Telemetry\ValueObjects\Metric;
use App\Framework\Telemetry\ValueObjects\Operation;
use Throwable;

/**
 * Unified telemetry service that provides a central point for collecting and exporting telemetry data
 */
#[Singleton]
final class UnifiedTelemetryService implements TelemetryService
{
    /**
     * @var array<TelemetryExporterInterface> Exporters for telemetry data
     */
    private array $exporters = [];

    /**
     * @var TelemetryContext Context for tracking operations
     */
    private TelemetryContext $context;

    /**
     * @param PerformanceCollectorInterface $performanceCollector Performance collector for timing operations
     * @param CircuitBreaker $circuitBreaker Circuit breaker for resilience
     * @param Logger $logger Logger for error reporting
     * @param Clock $clock Clock for timing operations
     * @param TelemetryConfig $config Configuration for telemetry
     */
    public function __construct(
        private readonly PerformanceCollectorInterface $performanceCollector,
        private readonly CircuitBreaker $circuitBreaker,
        private readonly Logger $logger,
        private readonly Clock $clock,
        private readonly TelemetryConfig $config
    ) {
        $this->context = new TelemetryContext();
    }

    /**
     * Add an exporter for telemetry data
     */
    public function addExporter(TelemetryExporterInterface $exporter): self
    {
        $this->exporters[] = $exporter;

        return $this;
    }

    /**
     * Start a new operation
     *
     * @param string $name Name of the operation
     * @param string $type Type of operation (e.g., http, database, custom)
     * @param array<string, mixed> $attributes Additional attributes for the operation
     */
    public function startOperation(string $name, string $type, array $attributes = []): OperationHandle
    {
        if (! $this->config->isEnabled()) {
            // Return a dummy handle if telemetry is disabled
            return new OperationHandle('disabled', $this);
        }

        try {
            // Generate a unique ID for the operation
            $operationId = $this->generateId();
            $parentId = $this->context->getCurrentOperationId();

            // Create the operation
            $operation = new Operation(
                id: $operationId,
                parentId: $parentId,
                name: $name,
                type: $type,
                startTime: $this->clock->now(),
                attributes: $attributes
            );

            // Store the operation in the context
            $this->context->setCurrentOperation($operation);

            // Start timing the operation
            $this->performanceCollector->startTiming(
                $operationId,
                $this->mapTypeToCategory($type),
                $attributes
            );

            // Return a handle for the operation
            return new OperationHandle($operationId, $this);
        } catch (Throwable $e) {
            // Log the error but don't throw - telemetry should not break the application
            $this->logger->error("Failed to start operation: {$e->getMessage()}", [
                'exception' => $e,
                'operation_name' => $name,
                'operation_type' => $type,
            ]);

            // Return a dummy handle
            return new OperationHandle('error', $this);
        }
    }

    /**
     * End an operation
     *
     * @param string $operationId ID of the operation to end
     * @param string $status Status of the operation (success, error)
     * @param string|null $errorMessage Error message if the operation failed
     */
    public function endOperation(string $operationId, ?string $status = null, ?string $errorMessage = null): void
    {
        if (! $this->config->isEnabled() || $operationId === 'disabled' || $operationId === 'error') {
            return;
        }

        try {
            // Get the operation from the context
            $operation = $this->context->getOperation($operationId);
            if ($operation === null) {
                return;
            }

            // End timing the operation
            $this->performanceCollector->endTiming($operationId);

            // Update the operation
            $operation->endTime = $this->clock->now();
            $operation->status = $status ?? 'success';
            $operation->errorMessage = $errorMessage;

            // Export the operation
            $this->exportOperation($operation);

            // Remove the operation from the context
            $this->context->removeOperation($operationId);

            // Restore the parent operation as the current operation
            $this->context->restoreParentOperation($operation->parentId);
        } catch (Throwable $e) {
            // Log the error but don't throw - telemetry should not break the application
            $this->logger->error("Failed to end operation: {$e->getMessage()}", [
                'exception' => $e,
                'operation_id' => $operationId,
            ]);
        }
    }

    /**
     * Add an attribute to an operation
     *
     * @param string $operationId ID of the operation
     * @param string $key Attribute key
     * @param mixed $value Attribute value
     */
    public function addOperationAttribute(string $operationId, string $key, mixed $value): void
    {
        if (! $this->config->isEnabled() || $operationId === 'disabled' || $operationId === 'error') {
            return;
        }

        try {
            $operation = $this->context->getOperation($operationId);
            if ($operation !== null) {
                $operation->addAttribute($key, $value);
            }
        } catch (Throwable $e) {
            // Log the error but don't throw - telemetry should not break the application
            $this->logger->error("Failed to add operation attribute: {$e->getMessage()}", [
                'exception' => $e,
                'operation_id' => $operationId,
                'key' => $key,
            ]);
        }
    }

    /**
     * Record a metric
     *
     * @param string $name Name of the metric
     * @param float $value Value of the metric
     * @param string $unit Unit of measurement (e.g., ms, bytes, count)
     * @param array<string, mixed> $attributes Additional attributes for the metric
     */
    public function recordMetric(string $name, float $value, string $unit = '', array $attributes = []): void
    {
        if (! $this->config->isEnabled()) {
            return;
        }

        try {
            // Add the current operation ID to the attributes
            $attributes = array_merge(
                $attributes,
                ['operation_id' => $this->context->getCurrentOperationId()]
            );

            // Create the metric
            $metric = new Metric(
                name: $name,
                value: $value,
                unit: $unit,
                type: 'gauge',
                timestamp: $this->clock->now(),
                attributes: $attributes
            );

            // Export the metric
            $this->exportMetric($metric);
        } catch (Throwable $e) {
            // Log the error but don't throw - telemetry should not break the application
            $this->logger->error("Failed to record metric: {$e->getMessage()}", [
                'exception' => $e,
                'metric_name' => $name,
                'metric_value' => $value,
            ]);
        }
    }

    /**
     * Record an event
     *
     * @param string $name Name of the event
     * @param array<string, mixed> $attributes Additional attributes for the event
     * @param string $severity Severity of the event (info, warning, error)
     */
    public function recordEvent(string $name, array $attributes = [], string $severity = 'info'): void
    {
        if (! $this->config->isEnabled()) {
            return;
        }

        try {
            // Add the current operation ID to the attributes
            $attributes = array_merge(
                $attributes,
                ['operation_id' => $this->context->getCurrentOperationId()]
            );

            // Create the event
            $event = new Event(
                name: $name,
                timestamp: $this->clock->now(),
                severity: $severity,
                attributes: $attributes
            );

            // Export the event
            $this->exportEvent($event);
        } catch (Throwable $e) {
            // Log the error but don't throw - telemetry should not break the application
            $this->logger->error("Failed to record event: {$e->getMessage()}", [
                'exception' => $e,
                'event_name' => $name,
                'event_severity' => $severity,
            ]);
        }
    }

    /**
     * Execute a function within the context of an operation
     *
     * @template T
     * @param string $name Name of the operation
     * @param string $type Type of operation
     * @param callable(): T $callback Function to execute
     * @param array<string, mixed> $attributes Additional attributes for the operation
     * @return T Result of the callback
     */
    public function trace(string $name, string $type, callable $callback, array $attributes = []): mixed
    {
        $operation = $this->startOperation($name, $type, $attributes);

        try {
            $result = $callback();
            $operation->end('success');

            return $result;
        } catch (Throwable $e) {
            $operation->fail($e->getMessage());

            throw $e;
        }
    }

    /**
     * Get the current operation stack as a string for debugging
     */
    public function getOperationStack(): string
    {
        return $this->context->getOperationStackAsString();
    }

    /**
     * Export an operation to all exporters
     */
    private function exportOperation(Operation $operation): void
    {
        foreach ($this->exporters as $exporter) {
            try {
                $exporter->exportOperation($operation);
            } catch (Throwable $e) {
                $this->logger->error("Failed to export operation: {$e->getMessage()}", [
                    'exception' => $e,
                    'operation_id' => $operation->id,
                ]);
            }
        }
    }

    /**
     * Export a metric to all exporters
     */
    private function exportMetric(Metric $metric): void
    {
        foreach ($this->exporters as $exporter) {
            try {
                $exporter->exportMetric($metric);
            } catch (Throwable $e) {
                $this->logger->error("Failed to export metric: {$e->getMessage()}", [
                    'exception' => $e,
                    'metric_name' => $metric->name,
                ]);
            }
        }
    }

    /**
     * Export an event to all exporters
     */
    private function exportEvent(Event $event): void
    {
        foreach ($this->exporters as $exporter) {
            try {
                $exporter->exportEvent($event);
            } catch (Throwable $e) {
                $this->logger->error("Failed to export event: {$e->getMessage()}", [
                    'exception' => $e,
                    'event_name' => $event->name,
                ]);
            }
        }
    }

    /**
     * Generate a unique ID for an operation
     */
    private function generateId(): string
    {
        return bin2hex(random_bytes(8));
    }

    /**
     * Map operation type to performance category
     */
    private function mapTypeToCategory(string $type): PerformanceCategory
    {
        return match($type) {
            'http' => PerformanceCategory::API,
            'database' => PerformanceCategory::DATABASE,
            'cache' => PerformanceCategory::CACHE,
            'view' => PerformanceCategory::VIEW,
            'system' => PerformanceCategory::SYSTEM,
            'template' => PerformanceCategory::TEMPLATE,
            'routing' => PerformanceCategory::ROUTING,
            'controller' => PerformanceCategory::CONTROLLER,
            'filesystem' => PerformanceCategory::FILESYSTEM,
            'security' => PerformanceCategory::SECURITY,
            'benchmark' => PerformanceCategory::BENCHMARK,
            default => PerformanceCategory::CUSTOM
        };
    }

    /**
     * Flush all exporters
     */
    public function flush(): void
    {
        foreach ($this->exporters as $exporter) {
            try {
                $exporter->flush();
            } catch (Throwable $e) {
                $this->logger->error("Failed to flush exporter: {$e->getMessage()}", [
                    'exception' => $e,
                ]);
            }
        }
    }

    /**
     * Check if telemetry is enabled
     */
    public function isEnabled(): bool
    {
        return $this->config->isEnabled();
    }

    /**
     * Get the telemetry configuration
     */
    public function getConfig(): TelemetryConfig
    {
        return $this->config;
    }

    /**
     * Destructor to ensure exporters are flushed
     */
    public function __destruct()
    {
        $this->flush();
    }
}
