<?php

namespace App\Services;

use App\Models\ApiRequestLog;
use Closure;
use CTApi\CTConfig;
use CTApi\CTLog;
use CTApi\Exceptions\CTConfigException;
use CTApi\Exceptions\CTConnectException;
use CTApi\Exceptions\CTPermissionException;
use CTApi\Models\Events\Event\EventAgendaRequest;
use CTApi\Models\Events\Event\EventRequest;
use CTApi\Models\Events\Song\SongRequest;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;

class ChurchToolsService
{
    private bool $apiConfigured = false;

    public function __construct(
        private readonly ?Closure $eventFetcher = null,
        private readonly ?Closure $songFetcher = null,
        private readonly ?Closure $agendaFetcher = null,
        private readonly ?Closure $eventServiceFetcher = null,
    ) {}

    public function sync(): array
    {
        $startedAt = Carbon::now();

        try {
            $eventsSummary = $this->syncEvents();
            $songsCount = $this->syncSongs();

            $summary = [
                'services_count' => $eventsSummary['services_count'],
                'songs_count' => $eventsSummary['songs_count'],
                'matched_songs_count' => $eventsSummary['matched_songs_count'],
                'unmatched_songs_count' => $eventsSummary['unmatched_songs_count'],
                'agenda_items_count' => $eventsSummary['agenda_items_count'],
                'catalog_songs_count' => $songsCount,
            ];

            $this->writeSyncLog('success', $summary, null, $startedAt, Carbon::now());

            return $summary;
        } catch (Throwable $exception) {
            $errorCategory = $this->classifyError($exception);

            Log::error('CTS Sync fehlgeschlagen', [
                'kategorie' => $errorCategory,
                'nachricht' => $exception->getMessage(),
                'exception_klasse' => $exception::class,
                'dauer_sekunden' => $startedAt->diffInSeconds(Carbon::now()),
                'trace' => Str::limit($exception->getTraceAsString(), 2000),
            ]);

            $summary = [
                'services_count' => 0,
                'songs_count' => 0,
                'matched_songs_count' => 0,
                'unmatched_songs_count' => 0,
                'agenda_items_count' => 0,
                'catalog_songs_count' => 0,
            ];

            $this->writeSyncLog('error', $summary, '['.$errorCategory.'] '.$exception->getMessage(), $startedAt, Carbon::now());

            throw $exception;
        }
    }

    public function syncEvents(): array
    {
        $events = $this->fetchEvents();

        $summary = [
            'services_count' => 0,
            'songs_count' => 0,
            'matched_songs_count' => 0,
            'unmatched_songs_count' => 0,
            'agenda_items_count' => 0,
        ];

        foreach ($events as $event) {
            try {
                $eventId = (int) ($event->getId() ?? 0);
                if ($eventId === 0) {
                    continue;
                }

                $serviceRoles = $this->extractServiceRoles($this->getEventServices($eventId));
                $service = $this->upsertService($event, $serviceRoles);

                $songSummary = $this->syncServiceAgendaSongs((int) $service->id, $eventId);

                $summary['services_count']++;
                $summary['songs_count'] += $songSummary['songs_count'];
                $summary['matched_songs_count'] += $songSummary['matched_songs_count'];
                $summary['unmatched_songs_count'] += $songSummary['unmatched_songs_count'];
                $summary['agenda_items_count'] += $songSummary['agenda_items_count'] ?? 0;
            } catch (\Throwable $e) {
                Log::warning('Sync-Fehler für Event', [
                    'event_id' => $event->getId() ?? 'unknown',
                    'error' => $e->getMessage(),
                ]);

                continue;
            }
        }

        return $summary;
    }

    public function syncSongs(): int
    {
        $songs = $this->fetchSongs();
        $count = 0;

        foreach ($songs as $song) {
            $ccliId = $this->normalizeCcli($song->getCcli() ?? null);
            $ctsSongId = (string) ($song->getId() ?? '');

            if ($ccliId !== null) {
                DB::table('songs')->updateOrInsert(
                    ['ccli_id' => $ccliId],
                    [
                        'cts_song_id' => $ctsSongId !== '' ? $ctsSongId : null,
                        'title' => (string) ($song->getName() ?? ''),
                        'updated_at' => Carbon::now(),
                        'created_at' => Carbon::now(),
                    ]
                );

                $count++;
            }

            if ($ctsSongId !== '' && $ccliId === null) {
                $existing = DB::table('songs')->where('cts_song_id', $ctsSongId)->first();

                if (! $existing) {
                    DB::table('songs')->insert([
                        'cts_song_id' => $ctsSongId,
                        'title' => (string) ($song->getName() ?? ''),
                        'created_at' => Carbon::now(),
                        'updated_at' => Carbon::now(),
                    ]);
                }

                $count++;
            }
        }

        return $count;
    }

    public function syncAgenda(int $eventId): mixed
    {
        $fetcher = $this->agendaFetcher ?? function (int $id): mixed {
            $this->configureApi();

            return EventAgendaRequest::fromEvent($id)->get();
        };

        return $this->logApiCall(
            'syncAgenda',
            'agenda',
            fn (): mixed => $fetcher($eventId),
            ['event_id' => $eventId]
        );
    }

    public function getEventServices(int $eventId): array
    {
        $fetcher = $this->eventServiceFetcher ?? function (int $id): array {
            $this->configureApi();

            $event = EventRequest::find($id);

            return $event?->getEventServices() ?? [];
        };

        return $this->logApiCall(
            'getEventServices',
            'event_services',
            fn (): array => $fetcher($eventId),
            ['event_id' => $eventId]
        );
    }

    private function fetchEvents(): array
    {
        $fetcher = $this->eventFetcher ?? function (): array {
            $this->configureApi();

            $futureEvents = EventRequest::where('from', Carbon::now()->toDateString())
                ->where('to', Carbon::now()->addMonths(3)->toDateString())
                ->get();

            $pastEvents = EventRequest::where('from', Carbon::now()->subMonths(3)->toDateString())
                ->where('to', Carbon::now()->subDay()->toDateString())
                ->get();

            $future = array_slice($futureEvents, 0, 10);
            $past = array_slice(array_reverse($pastEvents), 0, 10);

            return array_merge($past, $future);
        };

        return $this->logApiCall('fetchEvents', 'events', fn (): array => $fetcher());
    }

    private function fetchSongs(): array
    {
        $fetcher = $this->songFetcher ?? function (): array {
            $this->configureApi();

            return SongRequest::all();
        };

        return $this->logApiCall('fetchSongs', 'songs', fn (): array => $fetcher());
    }

    private function logApiCall(string $method, string $endpoint, Closure $operation, ?array $context = null): mixed
    {
        $start = microtime(true);

        try {
            $result = $operation();
            $duration = (int) ((microtime(true) - $start) * 1000);

            ApiRequestLog::create([
                'method' => $method,
                'endpoint' => $endpoint,
                'status' => 'success',
                'request_context' => $context,
                'response_summary' => $this->summarizeResponse($result),
                'response_body' => $this->serializeResponseBody($result),
                'duration_ms' => $duration,
            ]);

            return $result;
        } catch (\Throwable $e) {
            $duration = (int) ((microtime(true) - $start) * 1000);
            $errorCategory = $this->classifyError($e);

            Log::error('CTS API Fehler bei '.$method, [
                'endpoint' => $endpoint,
                'kategorie' => $errorCategory,
                'nachricht' => $e->getMessage(),
                'exception_klasse' => $e::class,
                'dauer_ms' => $duration,
                'kontext' => $context,
                'trace' => Str::limit($e->getTraceAsString(), 2000),
            ]);

            ApiRequestLog::create([
                'method' => $method,
                'endpoint' => $endpoint,
                'status' => 'error',
                'request_context' => $context,
                'error_message' => '['.$errorCategory.'] '.$e->getMessage(),
                'duration_ms' => $duration,
            ]);

            throw $e;
        }
    }

    private function summarizeResponse(mixed $result): ?string
    {
        if ($result === null) {
            return null;
        }

        if (is_array($result)) {
            return 'Array mit '.count($result).' Eintraegen';
        }

        if (is_scalar($result)) {
            return Str::limit((string) $result, 500);
        }

        if (is_object($result)) {
            return 'Objekt vom Typ '.$result::class;
        }

        return null;
    }

    private function serializeResponseBody(mixed $result): ?string
    {
        if ($result === null) {
            return null;
        }

        try {
            $json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);

            return mb_strlen($json) > 512000 ? mb_substr($json, 0, 512000)."\n... (abgeschnitten)" : $json;
        } catch (\JsonException) {
            return null;
        }
    }

    private function configureApi(): void
    {
        if ($this->apiConfigured) {
            return;
        }

        $apiUrl = (string) Config::get('services.churchtools.url', '');
        $apiToken = (string) Config::get('services.churchtools.api_token', '');

        Log::debug('CTS API Konfiguration', [
            'url_gesetzt' => $apiUrl !== '',
            'url' => $apiUrl !== '' ? $apiUrl : '(leer)',
            'token_gesetzt' => $apiToken !== '',
            'token_laenge' => mb_strlen($apiToken),
        ]);

        if ($apiUrl === '') {
            Log::error('CTS API URL fehlt. Bitte CTS_API_URL in der .env Datei setzen.');
            throw new \RuntimeException('CTS API URL ist nicht konfiguriert. Bitte CTS_API_URL in der .env Datei setzen.');
        }

        if ($apiToken === '') {
            Log::error('CTS API Token fehlt. Bitte CTS_API_TOKEN in der .env Datei setzen.');
            throw new \RuntimeException('CTS API Token ist nicht konfiguriert. Bitte CTS_API_TOKEN in der .env Datei setzen.');
        }

        CTConfig::setApiUrl(rtrim($apiUrl, '/'));
        CTConfig::setApiKey($apiToken);
        CTLog::enableFileLog(false);

        $this->apiConfigured = true;
    }

    private function upsertService(mixed $event, array $serviceRoles): object
    {
        $ctsEventId = (string) $event->getId();

        DB::table('services')->updateOrInsert(
            ['cts_event_id' => $ctsEventId],
            [
                'title' => (string) ($event->getName() ?? ''),
                'date' => Carbon::parse((string) $event->getStartDate())->toDateString(),
                'preacher_name' => $serviceRoles['preacher'],
                'beamer_tech_name' => $serviceRoles['beamer_technician'],
                'last_synced_at' => Carbon::now(),
                'cts_data' => json_encode($this->extractRawData($event), JSON_THROW_ON_ERROR),
                'updated_at' => Carbon::now(),
                'created_at' => Carbon::now(),
            ]
        );

        return DB::table('services')
            ->where('cts_event_id', $ctsEventId)
            ->firstOrFail();
    }

    /** @deprecated Use syncServiceAgendaItems() instead. */
    private function syncServiceAgendaSongs(int $serviceId, int $eventId): array
    {
        return $this->syncServiceAgendaItems($serviceId, $eventId);
    }

    private function syncServiceAgendaItems(int $serviceId, int $eventId): array
    {
        try {
            $agenda = $this->syncAgenda($eventId);
        } catch (CTRequestException $e) {
            if (str_contains(Str::lower($e->getMessage()), 'not found')) {
                DB::table('services')
                    ->where('id', $serviceId)
                    ->update(['has_agenda' => false]);

                Log::info('Event hat keine Agenda', ['event_id' => $eventId]);

                return [
                    'songs_count' => 0,
                    'matched_songs_count' => 0,
                    'unmatched_songs_count' => 0,
                    'agenda_items_count' => 0,
                ];
            }

            throw $e;
        }

        DB::table('services')
            ->where('id', $serviceId)
            ->update(['has_agenda' => true]);

        $agendaItems = method_exists($agenda, 'getItems') ? $agenda->getItems() : [];

        $summary = [
            'songs_count' => 0,
            'matched_songs_count' => 0,
            'unmatched_songs_count' => 0,
            'agenda_items_count' => 0,
        ];

        $songMatchingService = app(SongMatchingService::class);
        $processedSortOrders = [];

        foreach ($agendaItems as $index => $item) {
            if (method_exists($item, 'getIsBeforeEvent') && $item->getIsBeforeEvent()) {
                continue;
            }

            $sortOrder = $index + 1;
            $processedSortOrders[] = $sortOrder;

            $responsible = method_exists($item, 'getResponsible') ? $item->getResponsible() : null;

            DB::table('service_agenda_items')->updateOrInsert(
                [
                    'service_id' => $serviceId,
                    'sort_order' => $sortOrder,
                ],
                [
                    'cts_agenda_item_id' => method_exists($item, 'getId') ? (string) ($item->getId() ?? '') : null,
                    'position' => (string) (method_exists($item, 'getPosition') ? ($item->getPosition() ?? '') : ''),
                    'title' => (string) (method_exists($item, 'getTitle') ? ($item->getTitle() ?? '') : ''),
                    'type' => (string) (method_exists($item, 'getType') ? ($item->getType() ?? '') : ''),
                    'note' => method_exists($item, 'getNote') ? $item->getNote() : null,
                    'duration' => method_exists($item, 'getDuration') ? $item->getDuration() : null,
                    'start' => method_exists($item, 'getStart') ? $item->getStart() : null,
                    'is_before_event' => false,
                    'responsible' => $responsible !== null ? json_encode($responsible, JSON_THROW_ON_ERROR) : null,
                    'updated_at' => Carbon::now(),
                    'created_at' => Carbon::now(),
                ]
            );

            $song = method_exists($item, 'getSong') ? $item->getSong() : null;

            if ($song !== null) {
                $ctsSongId = (string) ($song->getId() ?? '');
                if ($ctsSongId !== '') {
                    $ccliId = $this->normalizeCcli($song->getCcli() ?? null);

                    DB::table('service_songs')->updateOrInsert(
                        [
                            'service_id' => $serviceId,
                            'order' => $sortOrder,
                        ],
                        [
                            'cts_song_name' => (string) ($song->getName() ?? ''),
                            'cts_ccli_id' => $ccliId,
                            'cts_song_id' => $ctsSongId,
                            'updated_at' => Carbon::now(),
                            'created_at' => Carbon::now(),
                        ]
                    );

                    $serviceSong = \App\Models\ServiceSong::where('service_id', $serviceId)
                        ->where('order', $sortOrder)
                        ->first();

                    if ($serviceSong) {
                        DB::table('service_agenda_items')
                            ->where('service_id', $serviceId)
                            ->where('sort_order', $sortOrder)
                            ->update(['service_song_id' => $serviceSong->id]);
                    }

                    $matched = false;

                    if (
                        $serviceSong !== null
                        && ! $serviceSong->song_id
                        && ($serviceSong->cts_ccli_id || $serviceSong->cts_song_id)
                    ) {
                        $matched = $songMatchingService->autoMatch($serviceSong);
                    } elseif ($serviceSong !== null && $serviceSong->song_id) {
                        $matched = true;
                    }

                    $summary['songs_count']++;

                    if ($matched) {
                        $summary['matched_songs_count']++;
                    } else {
                        $summary['unmatched_songs_count']++;
                    }
                }
            }

            $summary['agenda_items_count']++;
        }

        // Clean up orphaned agenda items: first null-ify slide FKs, then delete
        $orphanedIds = DB::table('service_agenda_items')
            ->where('service_id', $serviceId)
            ->whereNotIn('sort_order', $processedSortOrders)
            ->pluck('id');

        if ($orphanedIds->isNotEmpty()) {
            DB::table('slides')
                ->whereIn('service_agenda_item_id', $orphanedIds)
                ->update(['service_agenda_item_id' => null]);

            DB::table('service_agenda_items')
                ->where('service_id', $serviceId)
                ->whereNotIn('sort_order', $processedSortOrders)
                ->delete();
        }

        return $summary;
    }

    private function extractServiceRoles(array $eventServices): array
    {
        $roles = [
            'preacher' => null,
            'beamer_technician' => null,
        ];

        foreach ($eventServices as $eventService) {
            $serviceName = Str::lower((string) ($eventService->getName() ?? ''));
            $personName = $this->extractPersonName($eventService);

            if ($personName === null) {
                continue;
            }

            if ($roles['preacher'] === null && (str_contains($serviceName, 'predigt') || str_contains($serviceName, 'preach'))) {
                $roles['preacher'] = $personName;
            }

            if ($roles['beamer_technician'] === null && str_contains($serviceName, 'beamer')) {
                $roles['beamer_technician'] = $personName;
            }
        }

        return $roles;
    }

    private function extractPersonName(mixed $eventService): ?string
    {
        if (! method_exists($eventService, 'getPerson')) {
            return null;
        }

        $person = $eventService->getPerson();
        if ($person === null) {
            return null;
        }

        if (method_exists($person, 'getName')) {
            $name = trim((string) $person->getName());

            if ($name !== '') {
                return $name;
            }
        }

        $firstName = method_exists($person, 'getFirstName') ? trim((string) $person->getFirstName()) : '';
        $lastName = method_exists($person, 'getLastName') ? trim((string) $person->getLastName()) : '';
        $fullName = trim($firstName.' '.$lastName);

        return $fullName === '' ? null : $fullName;
    }

    private function extractRawData(mixed $event): array
    {
        if (method_exists($event, 'extractData')) {
            return $event->extractData();
        }

        return [
            'id' => $event->getId() ?? null,
            'title' => $event->getName() ?? null,
            'startDate' => $event->getStartDate() ?? null,
            'note' => $event->getNote() ?? null,
        ];
    }

    private function normalizeCcli(?string $ccli): ?string
    {
        if ($ccli === null) {
            return null;
        }

        $value = trim($ccli);

        return $value === '' ? null : $value;
    }

    private function classifyError(Throwable $e): string
    {
        if ($e instanceof CTPermissionException) {
            return 'authentifizierung';
        }

        if ($e instanceof CTConnectException) {
            return 'verbindung';
        }

        if ($e instanceof CTConfigException) {
            return 'konfiguration';
        }

        $message = Str::lower($e->getMessage());

        if (str_contains($message, 'token') || str_contains($message, '401') || str_contains($message, 'unauthorized')) {
            return 'authentifizierung';
        }

        if (str_contains($message, 'could not resolve') || str_contains($message, 'connection refused') || str_contains($message, 'timed out')) {
            return 'verbindung';
        }

        if (str_contains($message, '403') || str_contains($message, 'forbidden')) {
            return 'berechtigung';
        }

        if (str_contains($message, '404') || str_contains($message, 'not found')) {
            return 'nicht_gefunden';
        }

        if (str_contains($message, '5')) {
            if (preg_match('/\b5\d{2}\b/', $message)) {
                return 'server_fehler';
            }
        }

        return 'unbekannt';
    }

    private function writeSyncLog(string $status, array $summary, ?string $message, mixed $startedAt, mixed $finishedAt): void
    {
        DB::table('cts_sync_log')->insert([
            'status' => $status,
            'synced_at' => $finishedAt,
            'events_count' => $summary['services_count'] ?? 0,
            'songs_count' => $summary['songs_count'] ?? 0,
            'error' => $message,
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
    }
}
