<?php

namespace App\Services;

use App\Services\DTO\ParsedCcliSection;
use App\Services\DTO\ParsedCcliSong;
use App\Support\CcliLabels;
use Closure;
use InvalidArgumentException;

final class CcliPasteParser
{
    public function __construct(
        private readonly ?Closure $sectionDetector = null,
        private readonly ?Closure $metadataDetector = null,
    ) {}

    public function parse(string $rawText): ParsedCcliSong
    {
        if (trim($rawText) === '') {
            throw new InvalidArgumentException('Keine Sektionen erkannt — bitte vollständige Liedseite einfügen.');
        }

        $lines = array_map(
            fn (string $line): string => trim($line),
            preg_split('/\r\n|\n|\r/', $rawText) ?: [],
        );

        $isSectionLabel = $this->sectionDetector ?? fn (string $line): bool => CcliLabels::isSectionLabel($line);
        $isMetadataLine = $this->metadataDetector ?? fn (string $line): bool => CcliLabels::isMetadataLine($line);

        $firstSectionIndex = null;
        foreach ($lines as $index => $line) {
            if ($line !== '' && $isSectionLabel($line)) {
                $firstSectionIndex = $index;
                break;
            }
        }

        if ($firstSectionIndex === null) {
            return $this->parseMetadataOnly($lines, $isMetadataLine);
        }

        $headerLines = array_values(array_filter(
            array_slice($lines, 0, $firstSectionIndex),
            fn (string $line): bool => $line !== '',
        ));

        $title = $headerLines[0] ?? '';
        $author = $headerLines[1] ?? null;
        $ccliId = null;
        $year = null;
        $copyrightText = null;
        $sections = [];
        $current = null;
        $previousLineWasBlank = false;
        $currentParagraphLineCount = 0;
        $currentParagraphStartedAfterBlank = false;

        foreach (array_slice($lines, $firstSectionIndex) as $line) {
            if ($line === '') {
                $previousLineWasBlank = true;
                $currentParagraphLineCount = 0;
                $currentParagraphStartedAfterBlank = false;

                continue;
            }

            if ($isMetadataLine($line)) {
                if ($author === null
                    && $current !== null
                    && $currentParagraphLineCount === 1
                    && $currentParagraphStartedAfterBlank
                ) {
                    $author = array_pop($current['lines']);
                    $currentParagraphLineCount = 0;
                    $currentParagraphStartedAfterBlank = false;
                }

                $extractedCcliId = CcliLabels::extractCcliId($line);
                if ($extractedCcliId !== null) {
                    $ccliId = $extractedCcliId;
                }

                if (str_contains($line, '©')) {
                    $copyrightText = $line;

                    if (preg_match('/©\s*(\d{4})/u', $line, $matches)) {
                        $year = $matches[1];
                    }
                }

                continue;
            }

            if ($isSectionLabel($line)) {
                if ($current !== null) {
                    $sections[] = $current;
                }

                $label = CcliLabels::parseLabel($line);
                if ($label === null) {
                    continue;
                }

                $current = [
                    'label' => $line,
                    'kind' => CcliLabels::normalizeLabelName($label['kind']),
                    'rawKind' => $label['kind'],
                    'number' => $label['number'],
                    'modifier' => $label['modifier'],
                    'lines' => [],
                ];
                $previousLineWasBlank = false;
                $currentParagraphLineCount = 0;
                $currentParagraphStartedAfterBlank = false;

                continue;
            }

            if ($current !== null) {
                if ($currentParagraphLineCount === 0) {
                    $currentParagraphStartedAfterBlank = $previousLineWasBlank;
                }

                $current['lines'][] = $line;
                $currentParagraphLineCount++;
                $previousLineWasBlank = false;
            }
        }

        if ($current !== null) {
            $sections[] = $current;
        }

        $parsedSections = $this->mergeTranslatedSections($sections);

        if ($parsedSections === []) {
            if (trim($title) === '' && ($ccliId === null || trim($ccliId) === '')) {
                throw new InvalidArgumentException('Keine Sektionen erkannt — bitte vollständige Liedseite einfügen.');
            }
        }

        return new ParsedCcliSong(
            title: $title,
            author: $author,
            ccliId: $ccliId,
            year: $year,
            copyrightText: $copyrightText,
            sourceUrl: null,
            sections: $parsedSections,
        );
    }

    /**
     * Build a song from a paste that contains no lyric sections at all.
     *
     * Allowed as long as it is a real CCLI page (title + CCLI number present);
     * otherwise there is genuinely no song and we throw.
     *
     * @param  string[]  $lines
     * @param  Closure(string): bool  $isMetadataLine
     */
    private function parseMetadataOnly(array $lines, Closure $isMetadataLine): ParsedCcliSong
    {
        $nonEmpty = array_values(array_filter(
            $lines,
            fn (string $line): bool => $line !== '',
        ));

        $ccliId = null;
        $year = null;
        $copyrightText = null;

        foreach ($nonEmpty as $line) {
            if (! $isMetadataLine($line)) {
                continue;
            }

            $extractedCcliId = CcliLabels::extractCcliId($line);
            if ($extractedCcliId !== null) {
                $ccliId = $extractedCcliId;
            }

            if (str_contains($line, '©')) {
                $copyrightText = $line;

                if (preg_match('/©\s*(\d{4})/u', $line, $matches)) {
                    $year = $matches[1];
                }
            }
        }

        $headerLines = array_values(array_filter(
            $nonEmpty,
            fn (string $line): bool => ! $isMetadataLine($line),
        ));

        $title = $headerLines[0] ?? '';
        $author = $headerLines[1] ?? null;

        if (trim($title) === '' && ($ccliId === null || trim($ccliId) === '')) {
            throw new InvalidArgumentException('Keine Sektionen erkannt — bitte vollständige Liedseite einfügen.');
        }

        return new ParsedCcliSong(
            title: $title,
            author: $author,
            ccliId: $ccliId,
            year: $year,
            copyrightText: $copyrightText,
            sourceUrl: null,
            sections: [],
        );
    }

    /**
     * @param  array<int, array{label: string, kind: string, rawKind: string, number: string|null, modifier: string|null, lines: string[]}>  $sections
     * @return ParsedCcliSection[]
     */
    private function mergeTranslatedSections(array $sections): array
    {
        $merged = [];
        $index = 0;

        while ($index < count($sections)) {
            $section = $sections[$index];
            $next = $sections[$index + 1] ?? null;
            $linesTranslated = null;

            if ($next !== null && $this->isTranslatedPair($section, $next)) {
                $linesTranslated = $next['lines'];
                $index++;
            }

            $merged[] = new ParsedCcliSection(
                label: $section['label'],
                kind: $section['kind'],
                number: $section['number'],
                modifier: $section['modifier'],
                lines: $section['lines'],
                linesTranslated: $linesTranslated,
            );

            $index++;
        }

        return $merged;
    }

    /**
     * @param  array{kind: string, rawKind: string, number: string|null}  $section
     * @param  array{kind: string, rawKind: string, number: string|null}  $next
     */
    private function isTranslatedPair(array $section, array $next): bool
    {
        return mb_strtolower($section['rawKind']) !== mb_strtolower($next['rawKind'])
            && $this->canonicalLabel($section) === $this->canonicalLabel($next);
    }

    /**
     * @param  array{kind: string, number: string|null}  $section
     */
    private function canonicalLabel(array $section): string
    {
        $label = trim($section['kind'].' '.($section['number'] ?? ''));

        return mb_strtolower(CcliLabels::normalizeLabelName($label));
    }
}
