"""Headers of a parsed UltraStar txt file."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import attrs

from usdb_syncer import errors

from .auxiliaries import BeatsPerMinute, replace_false_apostrophes
from .language_translations import LANGUAGE_TRANSLATIONS

if TYPE_CHECKING:
    from collections.abc import Callable

    from usdb_syncer.logger import Logger
    from usdb_syncer.meta_tags import MetaTags
    from usdb_syncer.settings import FormatVersion


@attrs.define
class Headers:
    """Ultrastar headers."""

    unknown: dict[str, str]
    title: str
    artist: str
    bpm: BeatsPerMinute
    gap: int = 0
    version: str | None = None
    language: str | None = None
    edition: str | None = None
    genre: str | None = None
    album: str | None = None
    year: str | None = None
    creator: str | None = None
    mp3: str | None = None
    audio: str | None = None
    audiourl: str | None = None
    vocals: str | None = None
    instrumental: str | None = None
    cover: str | None = None
    coverurl: str | None = None
    background: str | None = None
    backgroundurl: str | None = None
    video: str | None = None
    videourl: str | None = None
    videogap: float | None = None
    start: float | None = None
    end: int | None = None
    previewstart: float | None = None
    relative: str | None = None
    p1: str | None = None
    p2: str | None = None
    medleystartbeat: int | None = None
    medleyendbeat: int | None = None
    # not rewritten, as it depends on the chosen encoding
    encoding: str | None = None
    comment: str | None = None
    providedby: str | None = None
    resolution: str | None = None
    tags: str | None = None

    @classmethod
    def parse(cls, lines: list[str], logger: Logger) -> Headers:
        """Consumes a stream of lines while they are headers."""
        kwargs: dict[str, Any] = {"unknown": {}}
        while lines:
            if not lines[0].startswith("#"):
                break
            line = lines.pop(0).removeprefix("#")
            if ":" not in line:
                logger.warning(f"header without value: '{line}'")
                continue
            header, value = line.split(":", maxsplit=1)
            if not value:
                # ignore headers with empty values
                continue
            try:
                _set_header_value(kwargs, header, value)
            except ValueError:
                logger.warning(f"invalid header value: '{line}'")
        if "title" not in kwargs or "artist" not in kwargs or "bpm" not in kwargs:
            raise errors.HeadersRequiredMissingError()
        return cls(**kwargs)

    def set_version(self, version: FormatVersion) -> None:
        self.version = version.value

    def reset_file_location_headers(self) -> None:
        """Clear all tags with local file locations."""
        self.mp3 = self.audio = self.vocals = self.instrumental = self.video = (
            self.cover
        ) = self.background = None

    def __str__(self) -> str:
        out = "\n".join(
            f"#{key.upper()}:{val}"
            for key in (
                "version",
                "title",
                "artist",
                "language",
                "edition",
                "genre",
                "year",
                "creator",
                "mp3",
                "audio",
                "audiourl",
                "vocals",
                "instrumental",
                "cover",
                "coverurl",
                "background",
                "backgroundurl",
                "video",
                "videourl",
                "videogap",
                "resolution",
                "start",
                "end",
                "relative",
                "previewstart",
                "medleystartbeat",
                "medleyendbeat",
                "bpm",
                "gap",
                "p1",
                "p2",
                "album",
                "comment",
                "providedby",
                "tags",
            )
            if (val := getattr(self, key)) is not None
        )
        if self.unknown:
            out = "\n".join(
                (out, *(f"#{key.upper()}:{val}" for key, val in self.unknown.items()))
            )
        return out

    def str_for_usdb(self) -> str:
        return "\n".join(
            f"#{key.upper()}:{val}"
            for key in (
                "artist",
                "title",
                "mp3",
                "creator",
                "edition",
                "cover",
                "background",
                "genre",
                "year",
                "language",
                "bpm",
                "gap",
                "video",
                "videogap",
                "start",
                "end",
            )
            if (val := getattr(self, key)) is not None
        )

    def artist_title_str(self) -> str:
        return f"{self.artist} - {self.title}"

    def fix_apostrophes(self, logger: Logger) -> None:
        apostrophes_and_quotation_marks_fixed = False
        for key in ("artist", "title", "language", "genre", "p1", "p2", "album"):
            if value := getattr(self, key):
                corrected_value = replace_false_apostrophes(value)
                if value != corrected_value:
                    setattr(self, key, corrected_value)
                    apostrophes_and_quotation_marks_fixed = True
        if apostrophes_and_quotation_marks_fixed:
            logger.debug("FIX: Apostrophes in song header corrected.")

    def apply_to_medley_tags(self, func: Callable[[int], int]) -> None:
        if self.medleystartbeat:
            self.medleystartbeat = func(self.medleystartbeat)
        if self.medleyendbeat:
            self.medleyendbeat = func(self.medleyendbeat)

    def fix_language(self, logger: Logger) -> None:
        if not self.language:
            logger.debug("No #LANGUAGE tag found. Consider adding it.")
        if old_language := self.language:
            languages = [
                language.strip()
                for language in self.language.replace(";", ",")
                .replace("/", ",")
                .replace("|", ",")
                .split(",")
            ]
            languages = [
                LANGUAGE_TRANSLATIONS.get(language.lower(), language)
                for language in languages
            ]
            self.language = ", ".join(languages)
            if old_language != self.language:
                logger.debug(f"FIX: Language corrected to {self.language}.")

    def main_language(self) -> str:
        if self.language:
            return self.language.split(",", maxsplit=1)[0].removesuffix(" (romanized)")
        return ""

    def fix_videogap(self, meta_tags: MetaTags, logger: Logger) -> None:
        if self.videogap is None:
            return

        if meta_tags.audio and meta_tags.video is None:
            logger.info(
                "This song is audio only, thus the #VIDEOGAP is without effect. "
                "Deleting #VIDEOGAP from local file."
            )
            self.videogap = None
        elif meta_tags.audio is None and meta_tags.video:
            logger.info(
                "This song (implicitly) uses audio and video from the same source, "
                "which are, generally, already in sync. Therefore, the #VIDEOGAP "
                "actually causes audio and video to be asynchronized. "
                "To fix this, the #VIDEOGAP is deleted from the local file. "
                "If the actual intention of the #VIDEOGAP is to sync audio and video "
                "from the *same* source, both 'a=' and 'v=' metatags need to be "
                "explicitly present and need to point to the same source. "
            )
            self.videogap = None
        elif meta_tags.audio and meta_tags.video and meta_tags.audio == meta_tags.video:
            logger.info(
                "This song (explicitly) uses audio and video from the same source. It "
                "is assumed that the #VIDEOGAP is intentional to actually fix the "
                "misalignment of audio and video in that source."
            )


def _set_header_value(kwargs: dict[str, Any], header: str, value: str) -> None:
    header = "creator" if header == "AUTHOR" else header.lower()
    if header in (
        "artist",
        "version",
        "language",
        "edition",
        "genre",
        "album",
        "year",
        "creator",
        "mp3",
        "audio",
        "audiourl",
        "vocals",
        "instrumental",
        "cover",
        "coverurl",
        "background",
        "backgroundurl",
        "video",
        "videourl",
        "relative",
        "p1",
        "p2",
        "encoding",
        "comment",
        "providedby",
        "resolution",
        "tags",
    ):
        kwargs[header] = value
    elif header == "title":
        kwargs[header] = value.removesuffix(" [DUET]")
    # these are given in (fractional) seconds, thus may have a decimal comma or point
    elif header in ("videogap", "start", "previewstart"):
        if (val := float(value.replace(",", "."))) != 0.0:
            kwargs[header] = val
    # these are given in milliseconds, but may have a decimal point or comma in usdb
    elif header in ("gap", "end"):
        kwargs[header] = round(float(value.replace(",", ".")))
    # these are given in beats and should thus be integers
    elif header in ("medleystartbeat", "medleyendbeat"):
        kwargs[header] = int(value)
    elif header == "bpm":
        kwargs[header] = BeatsPerMinute.parse(value)
    else:
        kwargs["unknown"][header] = value
