"""Audio metadata reading, writing, and artwork helpers."""

from __future__ import annotations

import json
import re
import unicodedata
from dataclasses import asdict, dataclass, field, fields
from pathlib import Path
from typing import Any

from .i18n import tr
from .lrc import lrc_path_for_audio
from .tag_schema import TAG_KEYS

SUPPORTED_EXTENSIONS = {
    ".aac",
    ".aif",
    ".aiff",
    ".alac",
    ".ape",
    ".flac",
    ".m4a",
    ".mp3",
    ".ogg",
    ".opus",
    ".wav",
    ".wma",
}

CORE_TAG_ATTRIBUTES = {
    "title": "title",
    "artist": "artist",
    "album": "album",
    "genre": "genre",
    "discnumber": "disc_number",
    "disctotal": "disc_total",
    "track": "track_number",
    "tracktotal": "track_total",
}

EASY_TAG_KEYS = {
    "contentgroup": "grouping",
    "mediatype": "media",
    "publisher": "organization",
    "track": "tracknumber",
    "www": "website",
    "origyear": "originaldate",
    # Keep the historical UI/configuration key for compatibility while using
    # the correctly-spelled standard metadata field on disk.
    "unsycedlyrics": "unsyncedlyrics",
}
APP_TAG_DESCRIPTION = "MusicGridStudio Tags"
APP_VORBIS_KEY = "MUSICGRIDSTUDIO_TAGS"
APP_MP4_KEY = "----:com.apple.iTunes:MusicGridStudio Tags"


def _first(tags: Any, key: str, default: str = "") -> str:
    if not tags:
        return default
    value = tags.get(key, default)
    if isinstance(value, (list, tuple)):
        value = value[0] if value else default
    return str(value) if value is not None else default


def _split_pair(value: str) -> tuple[str, str]:
    left, separator, right = value.partition("/")
    return left.strip(), right.strip() if separator else ""


def _tag_value(tags: Any, easy_key: str, id3_key: str, default: str = "") -> str:
    value = _first(tags, easy_key)
    if value:
        return value
    frame = tags.get(id3_key) if tags else None
    if frame is not None:
        text = getattr(frame, "text", frame)
        if isinstance(text, (list, tuple)):
            text = text[0] if text else ""
        if text:
            return str(text)
    return default


def format_duration(milliseconds: int) -> str:
    seconds = max(0, milliseconds // 1000)
    hours, remainder = divmod(seconds, 3600)
    minutes, seconds = divmod(remainder, 60)
    if hours:
        return f"{hours}:{minutes:02d}:{seconds:02d}"
    return f"{minutes}:{seconds:02d}"


def sanitize_filename_component(value: str) -> str:
    """Return a conservative Windows/CP932-compatible filename component."""
    normalized = unicodedata.normalize("NFKC", value)
    forbidden = set('<>:"/\\|?*')
    characters: list[str] = []
    for character in normalized:
        if character in forbidden or unicodedata.category(character).startswith("C"):
            characters.append(" ")
            continue
        try:
            character.encode("cp932")
        except UnicodeEncodeError:
            characters.append(" ")
        else:
            characters.append(character)
    cleaned = re.sub(r"\s+", " ", "".join(characters)).strip(" .")
    reserved = {
        "CON",
        "PRN",
        "AUX",
        "NUL",
        *(f"COM{number}" for number in range(1, 10)),
        *(f"LPT{number}" for number in range(1, 10)),
    }
    if cleaned.upper() in reserved:
        cleaned = f"_{cleaned}"
    return cleaned[:180].rstrip(" .")


def _filename_tag_number(value: str, fallback: int) -> str:
    number = value.strip()
    if number.isdecimal():
        return f"{int(number):02d}"
    number = sanitize_filename_component(number)
    return number or f"{fallback:02d}"


def build_tag_filename(
    track: "Track",
    fallback_track_number: int,
    pattern: str = "track_title",
) -> str:
    """Build ``01_Title.ext`` or ``01_01_Title.ext`` from tag fields."""

    track_number = _filename_tag_number(
        track.track_number,
        fallback_track_number,
    )
    title = sanitize_filename_component(track.title)
    if not title:
        title = sanitize_filename_component(Path(track.path).stem) or tr(
            "Untitled", "無題"
        )
    extension = Path(track.path).suffix.lower()
    if pattern == "disc_track_title":
        disc_number = _filename_tag_number(track.disc_number, 1)
        return f"{disc_number}_{track_number}_{title}{extension}"
    return f"{track_number}_{title}{extension}"


@dataclass(slots=True)
class Track:
    path: str
    filename: str
    title: str = ""
    artist: str = ""
    album: str = ""
    genre: str = ""
    disc_number: str = ""
    disc_total: str = ""
    track_number: str = ""
    track_total: str = ""
    duration_ms: int = 0
    sample_rate: int = 0
    bits_per_sample: int = 0
    channels: int = 0
    bitrate_bps: int = 0
    codec: str = ""
    cue_path: str = ""
    cue_start_ms: int = 0
    cue_end_ms: int = 0
    cue_tags: dict[str, str] = field(default_factory=dict)
    custom_tags: dict[str, str | int] = field(default_factory=dict)

    @classmethod
    def from_path(cls, path: str | Path) -> "Track":
        source = Path(path).resolve()
        track = cls(
            path=str(source),
            filename=source.name,
            title=source.stem,
            codec=source.suffix.lstrip(".").upper(),
        )
        try:
            import mutagen

            audio = mutagen.File(source, easy=True)
            if audio is None:
                return track
            tags = audio.tags
            track.title = _tag_value(tags, "title", "TIT2", source.stem)
            track.artist = _tag_value(tags, "artist", "TPE1")
            track.album = _tag_value(tags, "album", "TALB")
            track.genre = _tag_value(tags, "genre", "TCON")
            track.disc_number, track.disc_total = _split_pair(
                _tag_value(tags, "discnumber", "TPOS")
            )
            if not track.disc_total:
                track.disc_total = _first(tags, "disctotal") or _first(
                    tags, "totaldiscs"
                )
            track.track_number, track.track_total = _split_pair(
                _tag_value(tags, "tracknumber", "TRCK")
            )
            if not track.track_total:
                track.track_total = _first(tags, "tracktotal") or _first(
                    tags, "totaltracks"
                )
            info = getattr(audio, "info", None)
            if info:
                track.duration_ms = int(float(getattr(info, "length", 0)) * 1000)
                track.sample_rate = int(getattr(info, "sample_rate", 0) or 0)
                track.bits_per_sample = int(
                    getattr(info, "bits_per_sample", 0)
                    or getattr(info, "bit_depth", 0)
                    or 0
                )
                track.channels = int(getattr(info, "channels", 0) or 0)
                track.bitrate_bps = int(getattr(info, "bitrate", 0) or 0)
                codec_name = getattr(info, "codec", "") or getattr(
                    info, "codec_description", ""
                )
                if codec_name:
                    track.codec = str(codec_name)
            if not track.bitrate_bps:
                if (
                    source.suffix.lower() in {".wav", ".aif", ".aiff"}
                    and track.sample_rate
                    and track.bits_per_sample
                    and track.channels
                ):
                    track.bitrate_bps = (
                        track.sample_rate
                        * track.bits_per_sample
                        * track.channels
                    )
                elif track.duration_ms:
                    track.bitrate_bps = int(
                        source.stat().st_size * 8 * 1000 / track.duration_ms
                    )
            for key in TAG_KEYS:
                if key in CORE_TAG_ATTRIBUTES or key == "rating wmp":
                    continue
                easy_key = EASY_TAG_KEYS.get(key, key)
                value = _first(tags, easy_key)
                if key == "unsycedlyrics" and not value:
                    value = _first(tags, "lyrics") or _first(
                        tags, "unsycedlyrics"
                    )
                if value:
                    track.custom_tags[key] = value
            track.custom_tags.update(_read_app_custom_tags(source))
        except Exception:
            # Unsupported or damaged files can still be queued and played by Qt.
            pass
        return track

    @property
    def duration_text(self) -> str:
        return format_duration(self.duration_ms)

    @property
    def is_cue_track(self) -> bool:
        return bool(self.cue_path)

    @property
    def playlist_identity(self) -> str:
        if self.is_cue_track:
            return (
                f"cue:{Path(self.cue_path).resolve()}|{Path(self.path).resolve()}|"
                f"{self.cue_start_ms}|{self.track_number}"
            ).casefold()
        return str(Path(self.path).resolve()).casefold()

    @property
    def quality_text(self) -> str:
        values: list[str] = []
        if self.sample_rate:
            values.append(f"{self.sample_rate / 1000:g} kHz")
        if self.bits_per_sample:
            values.append(f"{self.bits_per_sample} bit")
        if self.channels:
            values.append(f"{self.channels} ch")
        return " / ".join(values) or "—"

    @property
    def bitrate_frequency_text(self) -> str:
        values: list[str] = []
        if self.bitrate_bps:
            values.append(f"{self.bitrate_bps / 1000:.0f} kbps")
        elif self.bits_per_sample:
            values.append(f"{self.bits_per_sample} bit")
        if self.sample_rate:
            values.append(f"{self.sample_rate / 1000:g} kHz")
        return "  •  ".join(values) or "—"

    def tag_value(self, key: str) -> str | int:
        attribute = CORE_TAG_ATTRIBUTES.get(key)
        if attribute:
            return getattr(self, attribute)
        if key == "rating wmp":
            try:
                return max(0, min(5, int(self.custom_tags.get(key, 0) or 0)))
            except (TypeError, ValueError):
                return 0
        return self.custom_tags.get(key, "")

    def set_tag_value(self, key: str, value: object) -> None:
        attribute = CORE_TAG_ATTRIBUTES.get(key)
        if attribute:
            setattr(self, attribute, str(value).strip())
            return
        if key == "rating wmp":
            try:
                rating = int(value)
            except (TypeError, ValueError):
                rating = 0
            self.custom_tags[key] = max(0, min(5, rating))
            return
        text = str(value).strip()
        if text:
            self.custom_tags[key] = text
        else:
            self.custom_tags.pop(key, None)

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)

    def to_playlist_entry(self) -> str | dict[str, Any]:
        if not self.is_cue_track:
            return self.path
        return {"kind": "cue_track", **self.to_dict()}

    @classmethod
    def from_dict(cls, values: dict[str, Any]) -> "Track":
        known = {definition.name for definition in fields(cls)}
        cleaned = {key: value for key, value in values.items() if key in known}
        for key in (
            "duration_ms",
            "sample_rate",
            "bits_per_sample",
            "channels",
            "bitrate_bps",
            "cue_start_ms",
            "cue_end_ms",
        ):
            try:
                cleaned[key] = int(cleaned.get(key, 0) or 0)
            except (TypeError, ValueError):
                cleaned[key] = 0
        raw_cue_tags = cleaned.get("cue_tags", {})
        cleaned["cue_tags"] = (
            {
                str(key): str(value)
                for key, value in raw_cue_tags.items()
            }
            if isinstance(raw_cue_tags, dict)
            else {}
        )
        raw_custom_tags = cleaned.get("custom_tags", {})
        normalized_custom_tags: dict[str, str | int] = {}
        if isinstance(raw_custom_tags, dict):
            for key, value in raw_custom_tags.items():
                normalized_key = str(key)
                if normalized_key == "rating wmp":
                    try:
                        normalized_custom_tags[normalized_key] = max(
                            0,
                            min(5, int(value or 0)),
                        )
                    except (TypeError, ValueError):
                        normalized_custom_tags[normalized_key] = 0
                else:
                    normalized_custom_tags[normalized_key] = str(value)
        cleaned["custom_tags"] = normalized_custom_tags
        return cls(**cleaned)


def _decode_custom_tag_payload(value: object) -> dict[str, str | int]:
    if isinstance(value, (list, tuple)):
        value = value[0] if value else ""
    if isinstance(value, bytes):
        value = value.decode("utf-8", errors="replace")
    try:
        decoded = json.loads(str(value or ""))
    except (TypeError, ValueError, json.JSONDecodeError):
        return {}
    if not isinstance(decoded, dict):
        return {}
    result: dict[str, str | int] = {}
    for key, raw_value in decoded.items():
        normalized_key = str(key).casefold()
        if normalized_key not in TAG_KEYS or normalized_key in CORE_TAG_ATTRIBUTES:
            continue
        if normalized_key == "rating wmp":
            try:
                result[normalized_key] = max(
                    0,
                    min(5, int(raw_value or 0)),
                )
            except (TypeError, ValueError):
                result[normalized_key] = 0
        else:
            result[normalized_key] = str(raw_value)
    return result


def _wmp_rating_from_byte(value: object) -> int:
    try:
        rating_byte = max(0, min(255, int(value or 0)))
    except (TypeError, ValueError):
        return 0
    if rating_byte == 0:
        return 0
    levels = (1, 64, 128, 196, 255)
    return min(range(1, 6), key=lambda rating: abs(levels[rating - 1] - rating_byte))


def _read_app_custom_tags(path: str | Path) -> dict[str, str | int]:
    try:
        import mutagen
        from mutagen.flac import FLAC
        from mutagen.mp4 import MP4

        audio = mutagen.File(path)
        if audio is None:
            return {}
        tags = getattr(audio, "tags", None)
        if not tags:
            return {}
        result: dict[str, str | int] = {}
        if isinstance(audio, MP4):
            lyrics = _first(tags, "\xa9lyr")
            if lyrics:
                result["unsycedlyrics"] = lyrics
            result.update(_decode_custom_tag_payload(tags.get(APP_MP4_KEY, [])))
            return result
        if isinstance(audio, FLAC) or Path(path).suffix.lower() in {".ogg", ".opus"}:
            lyrics = (
                _first(tags, "unsyncedlyrics")
                or _first(tags, "lyrics")
                or _first(tags, "unsycedlyrics")
            )
            if lyrics:
                result["unsycedlyrics"] = lyrics
            result.update(
                _decode_custom_tag_payload(tags.get(APP_VORBIS_KEY, []))
            )
            rating = _first(tags, "rating_wmp")
            if rating:
                try:
                    result["rating wmp"] = max(0, min(5, int(rating)))
                except (TypeError, ValueError):
                    pass
            return result
        getall = getattr(tags, "getall", None)
        if callable(getall):
            lyric_frames = list(getall("USLT"))
            if lyric_frames:
                lyrics = str(getattr(lyric_frames[0], "text", "") or "")
                if lyrics:
                    result["unsycedlyrics"] = lyrics
            for frame in getall("TXXX"):
                if getattr(frame, "desc", "") == APP_TAG_DESCRIPTION:
                    result.update(
                        _decode_custom_tag_payload(getattr(frame, "text", []))
                    )
            for frame in getall("POPM"):
                if getattr(frame, "email", "") == "Windows Media Player 9 Series":
                    result["rating wmp"] = _wmp_rating_from_byte(
                        getattr(frame, "rating", 0)
                    )
                    break
        return result
    except Exception:
        return {}


def _custom_tag_payload(track: Track) -> str:
    values = {
        key: value
        for key, value in track.custom_tags.items()
        if key in TAG_KEYS and key not in CORE_TAG_ATTRIBUTES and value not in {"", None}
    }
    return json.dumps(values, ensure_ascii=False, separators=(",", ":"))


def _save_easy_custom_tags(track: Track) -> None:
    suffix = Path(track.path).suffix.lower()
    if suffix in {".wav", ".aif", ".aiff"}:
        return
    try:
        import mutagen

        audio = mutagen.File(track.path, easy=True)
        if audio is None or audio.tags is None:
            return
        changed = False
        for key in TAG_KEYS:
            if key in CORE_TAG_ATTRIBUTES or key == "rating wmp":
                continue
            easy_key = EASY_TAG_KEYS.get(key, key)
            value = track.custom_tags.get(key, "")
            try:
                if value not in {"", None}:
                    audio[easy_key] = [str(value)]
                    changed = True
                elif easy_key in audio:
                    del audio[easy_key]
                    changed = True
            except (KeyError, TypeError, ValueError):
                continue
        if changed:
            audio.save()
    except Exception:
        # Unsupported standard mappings are still preserved in the app's
        # format-specific custom payload below.
        return


def _save_app_custom_tags(track: Track) -> None:
    import mutagen
    from mutagen.flac import FLAC
    from mutagen.id3 import POPM, TXXX
    from mutagen.mp4 import MP4, MP4FreeForm

    audio = mutagen.File(track.path)
    if audio is None:
        return
    tags = getattr(audio, "tags", None)
    if tags is None:
        add_tags = getattr(audio, "add_tags", None)
        if callable(add_tags):
            add_tags()
            tags = audio.tags
    if tags is None:
        return
    payload = _custom_tag_payload(track)
    suffix = Path(track.path).suffix.lower()
    if isinstance(audio, MP4):
        tags[APP_MP4_KEY] = [MP4FreeForm(payload.encode("utf-8"))]
        audio.save()
        return
    if isinstance(audio, FLAC) or suffix in {".ogg", ".opus"}:
        tags[APP_VORBIS_KEY] = [payload]
        rating = track.tag_value("rating wmp")
        tags["rating_wmp"] = [str(rating)]
        audio.save()
        return
    getall = getattr(tags, "getall", None)
    delall = getattr(tags, "delall", None)
    add = getattr(tags, "add", None)
    if not callable(getall) or not callable(delall) or not callable(add):
        return
    for frame in list(getall("TXXX")):
        if getattr(frame, "desc", "") == APP_TAG_DESCRIPTION:
            try:
                del tags[frame.HashKey]
            except KeyError:
                pass
    add(TXXX(encoding=3, desc=APP_TAG_DESCRIPTION, text=[payload]))
    for frame in list(getall("POPM")):
        if getattr(frame, "email", "") == "Windows Media Player 9 Series":
            try:
                del tags[frame.HashKey]
            except KeyError:
                pass
    rating = int(track.tag_value("rating wmp") or 0)
    if rating:
        rating_byte = (1, 64, 128, 196, 255)[rating - 1]
        add(
            POPM(
                email="Windows Media Player 9 Series",
                rating=rating_byte,
                count=0,
            )
        )
    if suffix == ".mp3":
        tags.save(track.path, v2_version=3)
    else:
        audio.save()


def discover_audio_files(paths: list[str]) -> list[str]:
    """Expand files and folders into a stable list of supported audio files."""
    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() not in SUPPORTED_EXTENSIONS:
                continue
            resolved = str(candidate.resolve())
            key = resolved.casefold()
            if key not in seen:
                seen.add(key)
                discovered.append(resolved)
    return discovered


def _pair(number: str, total: str) -> str:
    number = number.strip()
    total = total.strip()
    return f"{number}/{total}" if total else number


def _set_id3_frames(tags: Any, track: Track) -> None:
    from mutagen.id3 import TALB, TCON, TIT2, TPE1, TPOS, TRCK, USLT

    frame_values = {
        "TIT2": (TIT2, track.title),
        "TPE1": (TPE1, track.artist),
        "TALB": (TALB, track.album),
        "TCON": (TCON, track.genre),
        "TPOS": (TPOS, _pair(track.disc_number, track.disc_total)),
        "TRCK": (TRCK, _pair(track.track_number, track.track_total)),
    }
    for frame_id, (frame_type, value) in frame_values.items():
        tags.delall(frame_id)
        if value:
            tags.add(frame_type(encoding=3, text=[value]))
    tags.delall("USLT")
    lyrics = str(track.tag_value("unsycedlyrics") or "")
    if lyrics:
        tags.add(USLT(encoding=3, lang="und", desc="", text=lyrics))


def _save_id3(track: Track) -> None:
    from mutagen.id3 import ID3, ID3NoHeaderError

    try:
        tags = ID3(track.path)
    except ID3NoHeaderError:
        tags = ID3()
    _set_id3_frames(tags, track)
    tags.save(track.path, v2_version=3)


def _save_vorbis(track: Track, audio: Any) -> None:
    values = {
        "title": track.title,
        "artist": track.artist,
        "album": track.album,
        "genre": track.genre,
        "discnumber": track.disc_number,
        "disctotal": track.disc_total,
        "tracknumber": track.track_number,
        "tracktotal": track.track_total,
        "unsyncedlyrics": str(track.tag_value("unsycedlyrics") or ""),
    }
    for key, value in values.items():
        if value:
            audio[key] = [value]
        elif key in audio:
            del audio[key]
    audio.save()


def _save_mp4(track: Track, audio: Any) -> None:
    if audio.tags is None:
        audio.add_tags()
    values = {
        "\xa9nam": [track.title] if track.title else None,
        "\xa9ART": [track.artist] if track.artist else None,
        "\xa9alb": [track.album] if track.album else None,
        "\xa9gen": [track.genre] if track.genre else None,
        "\xa9lyr": [str(track.tag_value("unsycedlyrics"))]
        if track.tag_value("unsycedlyrics")
        else None,
        "disk": [
            (
                int(track.disc_number or 0),
                int(track.disc_total or 0),
            )
        ],
        "trkn": [
            (
                int(track.track_number or 0),
                int(track.track_total or 0),
            )
        ],
    }
    for key, value in values.items():
        if value and (key not in {"disk", "trkn"} or value[0] != (0, 0)):
            audio.tags[key] = value
        else:
            audio.tags.pop(key, None)
    audio.save()


def save_track_tags(track: Track) -> str:
    """Write editable fields, then rename the audio and its LRC sidecar."""
    # A CUE row is a virtual interval inside a shared disc-image file. Writing
    # ordinary file tags or renaming the image for one row would affect every
    # other CUE row, so its edited display metadata is persisted in the
    # playlist instead.
    if track.is_cue_track:
        return track.path

    import mutagen
    from mutagen.flac import FLAC
    from mutagen.mp4 import MP4

    suffix = Path(track.path).suffix.lower()
    if suffix == ".mp3":
        _save_id3(track)
    elif suffix == ".wav":
        from mutagen.wave import WAVE

        audio = WAVE(track.path)
        if audio.tags is None:
            audio.add_tags()
        _set_id3_frames(audio.tags, track)
        audio.save()
    elif suffix in {".aif", ".aiff"}:
        from mutagen.aiff import AIFF

        audio = AIFF(track.path)
        if audio.tags is None:
            audio.add_tags()
        _set_id3_frames(audio.tags, track)
        audio.save()
    else:
        audio = mutagen.File(track.path)
        if audio is None:
            raise ValueError(
                tr(
                    "Tags cannot be read from or written to this file format.",
                    "このファイル形式のタグを読み書きできません。",
                )
            )
        if isinstance(audio, FLAC) or suffix in {".ogg", ".opus"}:
            _save_vorbis(track, audio)
        elif isinstance(audio, MP4):
            _save_mp4(track, audio)
        else:
            unknown = tr("unknown format", "不明な形式")
            raise ValueError(
                tr(
                    f"Saving tags to {suffix or unknown} is not supported.",
                    f"{suffix or unknown} のタグ保存は未対応です。",
                )
            )

    _save_easy_custom_tags(track)
    _save_app_custom_tags(track)

    requested_name = Path(track.filename).name.strip()
    current = Path(track.path)
    if not requested_name:
        requested_name = current.name
    if not Path(requested_name).suffix:
        requested_name += current.suffix
    destination = current.with_name(requested_name)
    if destination != current:
        if destination.exists():
            raise FileExistsError(
                tr(
                    f"A file with the same name already exists: {destination.name}",
                    f"同名のファイルが既にあります: {destination.name}",
                )
            )
        current_lrc = lrc_path_for_audio(current)
        destination_lrc = lrc_path_for_audio(destination)
        move_lrc = current_lrc.is_file() and destination_lrc != current_lrc
        if move_lrc and destination_lrc.exists():
            raise FileExistsError(
                tr(
                    "A synchronized lyrics file with the destination name "
                    f"already exists: {destination_lrc.name}",
                    "変更先と同名の同期歌詞ファイルが既にあります: "
                    f"{destination_lrc.name}",
                )
            )
        current.rename(destination)
        try:
            if move_lrc:
                current_lrc.rename(destination_lrc)
        except Exception as lrc_error:
            try:
                destination.rename(current)
            except OSError as rollback_error:
                track.path = str(destination.resolve())
                track.filename = destination.name
                raise RuntimeError(
                    tr(
                        "The audio file was renamed, but its synchronized lyrics "
                        "could not be renamed and the audio filename could not be "
                        "restored. Check both files manually.",
                        "音源ファイル名は変更されましたが、同期歌詞ファイルの名称変更と"
                        "音源名の復元に失敗しました。両方のファイルを手動で確認してください。",
                    )
                ) from rollback_error
            raise lrc_error
        track.path = str(destination.resolve())
        track.filename = destination.name
    return track.path


def read_artwork(path: str) -> tuple[bytes, str] | None:
    import mutagen
    from mutagen.flac import FLAC
    from mutagen.mp4 import MP4

    audio = mutagen.File(path)
    if audio is None:
        return None
    if isinstance(audio, FLAC) and audio.pictures:
        picture = audio.pictures[0]
        return picture.data, picture.mime or "image/jpeg"
    if isinstance(audio, MP4) and audio.tags:
        covers = audio.tags.get("covr", [])
        if covers:
            cover_data = bytes(covers[0])
            return cover_data, _detect_image_mime(cover_data, "")
    tags = getattr(audio, "tags", None)
    if tags:
        for frame in tags.values():
            if getattr(frame, "FrameID", "") == "APIC":
                return bytes(frame.data), frame.mime or "image/jpeg"
    return None


def artwork_file_extension(data: bytes, mime: str = "") -> str:
    """Return the original image extension without decoding or re-encoding it."""

    if data.startswith(b"\xff\xd8\xff"):
        return ".jpg"
    if data.startswith(b"\x89PNG\r\n\x1a\n"):
        return ".png"
    if data.startswith((b"GIF87a", b"GIF89a")):
        return ".gif"
    if data.startswith(b"BM"):
        return ".bmp"
    if data.startswith(b"RIFF") and data[8:12] == b"WEBP":
        return ".webp"
    if data.startswith((b"II*\x00", b"MM\x00*")):
        return ".tif"
    normalized_mime = str(mime or "").split(";", 1)[0].strip().casefold()
    return {
        "image/jpeg": ".jpg",
        "image/jpg": ".jpg",
        "image/png": ".png",
        "image/gif": ".gif",
        "image/bmp": ".bmp",
        "image/webp": ".webp",
        "image/tiff": ".tif",
    }.get(normalized_mime, ".jpg")


def _detect_image_mime(data: bytes, filename: str) -> str:
    if data.startswith(b"\x89PNG"):
        return "image/png"
    if data.startswith(b"GIF8"):
        return "image/gif"
    if data.startswith(b"BM"):
        return "image/bmp"
    if Path(filename).suffix.lower() == ".png":
        return "image/png"
    return "image/jpeg"


def embed_artwork(audio_path: str, image_path: str) -> None:
    import mutagen
    from mutagen.flac import FLAC, Picture
    from mutagen.id3 import APIC, ID3, ID3NoHeaderError
    from mutagen.mp4 import MP4, MP4Cover

    data = Path(image_path).read_bytes()
    mime = _detect_image_mime(data, image_path)
    suffix = Path(audio_path).suffix.lower()

    if suffix == ".mp3":
        try:
            tags = ID3(audio_path)
        except ID3NoHeaderError:
            tags = ID3()
        tags.delall("APIC")
        tags.add(
            APIC(
                encoding=3,
                mime=mime,
                type=3,
                desc="Cover",
                data=data,
            )
        )
        tags.save(audio_path, v2_version=3)
        return
    if suffix == ".wav":
        from mutagen.wave import WAVE

        audio = WAVE(audio_path)
        if audio.tags is None:
            audio.add_tags()
        audio.tags.delall("APIC")
        audio.tags.add(
            APIC(
                encoding=3,
                mime=mime,
                type=3,
                desc="Cover",
                data=data,
            )
        )
        audio.save()
        return
    if suffix in {".aif", ".aiff"}:
        from mutagen.aiff import AIFF

        audio = AIFF(audio_path)
        if audio.tags is None:
            audio.add_tags()
        audio.tags.delall("APIC")
        audio.tags.add(
            APIC(
                encoding=3,
                mime=mime,
                type=3,
                desc="Cover",
                data=data,
            )
        )
        audio.save()
        return

    audio = mutagen.File(audio_path)
    if isinstance(audio, FLAC):
        picture = Picture()
        picture.type = 3
        picture.mime = mime
        picture.desc = "Cover"
        picture.data = data
        audio.clear_pictures()
        audio.add_picture(picture)
        audio.save()
        return
    if isinstance(audio, MP4):
        if audio.tags is None:
            audio.add_tags()
        image_format = (
            MP4Cover.FORMAT_PNG if mime == "image/png" else MP4Cover.FORMAT_JPEG
        )
        audio.tags["covr"] = [MP4Cover(data, imageformat=image_format)]
        audio.save()
        return
    unknown = tr("unknown format", "不明な形式")
    raise ValueError(
        tr(
            f"Embedding artwork in {suffix or unknown} is not supported.",
            f"{suffix or unknown} への画像埋め込みは未対応です。",
        )
    )


def remove_artwork(audio_path: str) -> bool:
    """Remove embedded front-cover images without changing other tags."""
    import mutagen
    from mutagen.flac import FLAC
    from mutagen.id3 import ID3, ID3NoHeaderError
    from mutagen.mp4 import MP4

    suffix = Path(audio_path).suffix.lower()
    if suffix == ".mp3":
        try:
            tags = ID3(audio_path)
        except ID3NoHeaderError:
            return False
        removed = bool(tags.getall("APIC"))
        if removed:
            tags.delall("APIC")
            tags.save(audio_path, v2_version=3)
        return removed
    if suffix == ".wav":
        from mutagen.wave import WAVE

        audio = WAVE(audio_path)
        if audio.tags is None:
            return False
        removed = bool(audio.tags.getall("APIC"))
        if removed:
            audio.tags.delall("APIC")
            audio.save()
        return removed
    if suffix in {".aif", ".aiff"}:
        from mutagen.aiff import AIFF

        audio = AIFF(audio_path)
        if audio.tags is None:
            return False
        removed = bool(audio.tags.getall("APIC"))
        if removed:
            audio.tags.delall("APIC")
            audio.save()
        return removed

    audio = mutagen.File(audio_path)
    if isinstance(audio, FLAC):
        removed = bool(audio.pictures)
        if removed:
            audio.clear_pictures()
            audio.save()
        return removed
    if isinstance(audio, MP4):
        if audio.tags is None or "covr" not in audio.tags:
            return False
        del audio.tags["covr"]
        audio.save()
        return True
    unknown = tr("unknown format", "不明な形式")
    raise ValueError(
        tr(
            f"Removing artwork from {suffix or unknown} is not supported.",
            f"{suffix or unknown} の画像削除は未対応です。",
        )
    )
