"""CUE-sheet parsing for virtual tracks backed by disc-image audio files."""

from __future__ import annotations

from dataclasses import replace
from pathlib import Path
import re
from typing import Any

from .metadata import SUPPORTED_EXTENSIONS, Track


class CueSheetError(ValueError):
    """Raised when a CUE sheet cannot be expanded into playable tracks."""


_COMMAND = re.compile(r"^\s*([A-Za-z][A-Za-z0-9_]*)\s*(.*?)\s*$")
_FILE = re.compile(r'^(?:"([^"]*)"|(\S+))(?:\s+(\S+))?\s*$')
_TRACK = re.compile(r"^(\d+)\s+(\S+)", re.IGNORECASE)
_INDEX = re.compile(r"^(\d+)\s+(\d+):(\d{1,2}):(\d{1,2})")
_REM = re.compile(r"^(\S+)(?:\s+(.*))?$", re.DOTALL)


def _decode_cue(path: Path) -> str:
    try:
        data = path.read_bytes()
    except OSError as exc:
        raise CueSheetError(f"Could not read the CUE sheet: {exc}") from exc
    encodings = (
        ("utf-8-sig", "utf-16", "cp932", "utf-8")
        if data.startswith((b"\xef\xbb\xbf", b"\xff\xfe", b"\xfe\xff"))
        else ("utf-8", "cp932", "utf-16")
    )
    for encoding in encodings:
        try:
            return data.decode(encoding)
        except UnicodeDecodeError:
            continue
    raise CueSheetError(
        "The CUE sheet text encoding is not supported (UTF-8/UTF-16/CP932)."
    )


def _unquote(value: str) -> str:
    value = value.strip()
    if len(value) >= 2 and value.startswith('"') and value.endswith('"'):
        return value[1:-1].replace('\\"', '"')
    return value


def _cue_time_ms(minutes: str, seconds: str, frames: str) -> int:
    minute_value = int(minutes)
    second_value = int(seconds)
    frame_value = int(frames)
    if second_value >= 60 or frame_value >= 75:
        raise CueSheetError(
            f"Invalid CUE index time: {minutes}:{seconds}:{frames}"
        )
    total_frames = ((minute_value * 60) + second_value) * 75 + frame_value
    return total_frames * 1000 // 75


def _resolve_audio_file(cue_path: Path, raw_name: str) -> Path:
    normalized_name = raw_name.replace("\\", "/")
    candidate = (cue_path.parent / normalized_name).resolve()
    if candidate.is_file():
        return candidate
    expected_name = Path(normalized_name).name.casefold()
    try:
        case_insensitive = next(
            (
                item.resolve()
                for item in cue_path.parent.iterdir()
                if item.is_file() and item.name.casefold() == expected_name
            ),
            None,
        )
    except OSError:
        case_insensitive = None
    if case_insensitive is not None:
        return case_insensitive
    raise CueSheetError(f"Referenced audio file was not found: {raw_name}")


def _record_tag(target: dict[str, str], command: str, value: str) -> None:
    if value:
        target[command.upper()] = _unquote(value)


def parse_cue_sheet(path: str | Path) -> list[Track]:
    """Expand one CUE sheet into independently manageable virtual tracks."""

    cue_path = Path(path).resolve()
    if not cue_path.is_file():
        raise CueSheetError(f"CUE sheet was not found: {cue_path}")

    album_tags: dict[str, str] = {}
    records: list[dict[str, Any]] = []
    current_file = ""
    current_file_type = ""
    current_track: dict[str, Any] | None = None

    for line_number, raw_line in enumerate(
        _decode_cue(cue_path).splitlines(),
        start=1,
    ):
        line = raw_line.strip()
        if not line:
            continue
        command_match = _COMMAND.match(line)
        if command_match is None:
            continue
        command = command_match.group(1).upper()
        value = command_match.group(2).strip()

        if command == "REM":
            rem_match = _REM.match(value)
            if rem_match:
                target = (
                    current_track["tags"]
                    if current_track is not None
                    else album_tags
                )
                _record_tag(
                    target,
                    f"REM_{rem_match.group(1)}",
                    rem_match.group(2) or "",
                )
            continue
        if command == "FILE":
            file_match = _FILE.match(value)
            if file_match is None:
                raise CueSheetError(
                    f"Invalid FILE command on line {line_number}."
                )
            current_file = file_match.group(1) or file_match.group(2) or ""
            current_file_type = file_match.group(3) or ""
            current_track = None
            continue
        if command == "TRACK":
            track_match = _TRACK.match(value)
            if track_match is None:
                raise CueSheetError(
                    f"Invalid TRACK command on line {line_number}."
                )
            if not current_file:
                raise CueSheetError(
                    f"TRACK appears before FILE on line {line_number}."
                )
            current_track = {
                "number": track_match.group(1),
                "type": track_match.group(2).upper(),
                "file": current_file,
                "file_type": current_file_type,
                "tags": {},
                "indexes": {},
            }
            records.append(current_track)
            continue
        if command == "INDEX" and current_track is not None:
            index_match = _INDEX.match(value)
            if index_match is None:
                raise CueSheetError(
                    f"Invalid INDEX command on line {line_number}."
                )
            current_track["indexes"][int(index_match.group(1))] = _cue_time_ms(
                index_match.group(2),
                index_match.group(3),
                index_match.group(4),
            )
            continue

        target = (
            current_track["tags"]
            if current_track is not None
            else album_tags
        )
        _record_tag(target, command, value)

    playable = [
        record
        for record in records
        if record["type"] == "AUDIO" and 1 in record["indexes"]
    ]
    if not playable:
        raise CueSheetError("The CUE sheet contains no AUDIO tracks with INDEX 01.")

    resolved_files: dict[str, Path] = {}
    base_tracks: dict[str, Track] = {}
    for record in playable:
        raw_file = str(record["file"])
        if raw_file not in resolved_files:
            audio_path = _resolve_audio_file(cue_path, raw_file)
            if audio_path.suffix.lower() not in SUPPORTED_EXTENSIONS:
                raise CueSheetError(
                    f"The referenced audio format is not supported: {audio_path.name}"
                )
            resolved_files[raw_file] = audio_path
            base_tracks[str(audio_path).casefold()] = Track.from_path(audio_path)
        record["resolved_file"] = resolved_files[raw_file]

    output: list[Track] = []
    total_text = f"{len(playable):02d}"
    for index, record in enumerate(playable):
        audio_path = record["resolved_file"]
        base = base_tracks[str(audio_path).casefold()]
        start_ms = int(record["indexes"][1])
        end_ms = base.duration_ms
        for following in playable[index + 1 :]:
            if following["resolved_file"] == audio_path:
                end_ms = int(following["indexes"][1])
                break
            break
        if end_ms and end_ms < start_ms:
            raise CueSheetError(
                f"TRACK {record['number']} starts after the end of its audio file."
            )

        tags = record["tags"]
        cue_tags = {
            key.casefold(): value
            for key, value in {**album_tags, **tags}.items()
        }
        cue_tags.update(
            {
                "cue_file": str(record["file"]),
                "cue_file_type": str(record["file_type"]),
                "cue_track": str(record["number"]),
                "index_01_ms": str(start_ms),
            }
        )
        if 0 in record["indexes"]:
            cue_tags["index_00_ms"] = str(record["indexes"][0])

        cue_track = replace(base)
        cue_track.filename = audio_path.name
        cue_track.title = tags.get("TITLE") or f"Track {record['number']}"
        cue_track.artist = (
            tags.get("PERFORMER")
            or album_tags.get("PERFORMER")
            or base.artist
        )
        cue_track.album = album_tags.get("TITLE") or base.album
        cue_track.genre = (
            tags.get("REM_GENRE")
            or album_tags.get("REM_GENRE")
            or base.genre
        )
        cue_track.track_number = f"{int(record['number']):02d}"
        cue_track.track_total = total_text
        cue_track.duration_ms = max(0, end_ms - start_ms) if end_ms else 0
        cue_track.cue_path = str(cue_path)
        cue_track.cue_start_ms = start_ms
        cue_track.cue_end_ms = end_ms
        cue_track.cue_tags = cue_tags
        output.append(cue_track)

    return output


def discover_cue_files(paths: list[str]) -> list[str]:
    """Expand files and folders into a stable, de-duplicated CUE list."""

    discovered: list[str] = []
    seen: set[str] = set()
    for raw_path in paths:
        path = Path(raw_path)
        candidates = (
            (item for item in path.rglob("*") if item.is_file())
            if path.is_dir()
            else (path,)
        )
        for candidate in candidates:
            if candidate.suffix.lower() != ".cue":
                continue
            resolved = str(candidate.resolve())
            key = resolved.casefold()
            if key not in seen:
                seen.add(key)
                discovered.append(resolved)
    return discovered
