from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import re
from typing import Literal, TypeAlias

from ._text import split_lines_with_endings

ModpackListReferenceKind: TypeAlias = Literal[
    "steam",
    "packageid",
    "local",
]


@dataclass(frozen=True)
class ModpackListReference:
    kind: ModpackListReferenceKind
    value: str


@dataclass(frozen=True)
class BlankLine:
    raw_text: str
    line_ending: str = ""
    kind: Literal["blank"] = "blank"


@dataclass(frozen=True)
class AliasLine:
    raw_text: str
    leading_whitespace: str
    whitespace_after_directive: str
    alias: str
    trailing_whitespace: str
    line_ending: str = ""
    kind: Literal["alias"] = "alias"


@dataclass(frozen=True)
class CommentLine:
    raw_text: str
    leading_whitespace: str
    comment_text: str
    line_ending: str = ""
    kind: Literal["comment"] = "comment"


@dataclass(frozen=True)
class EntryLine:
    raw_text: str
    leading_whitespace: str
    disabled: bool
    whitespace_after_disabled: str
    raw_reference_text: str
    reference: ModpackListReference
    spacing_before_comment: str
    trailing_comment_text: str | None
    line_ending: str = ""
    kind: Literal["entry"] = "entry"


@dataclass(frozen=True)
class UnknownLine:
    raw_text: str
    line_ending: str = ""
    kind: Literal["unknown"] = "unknown"


ModpackListLine: TypeAlias = (
    BlankLine | AliasLine | CommentLine | EntryLine | UnknownLine
)


@dataclass(frozen=True)
class ModpackListDocument:
    lines: tuple[ModpackListLine, ...]
    source_text: str = ""


_ENTRY_LINE_RE = re.compile(
    (
        r"^(?P<leading_whitespace>\s*)"
        + r"(?P<disabled>!)?"
        + r"(?P<whitespace_after_disabled>\s*)"
        + r"(?P<raw_reference_text>\S+?)"
        + r"(?P<spacing_before_comment>\s*)"
        + r"(?P<trailing_comment>#.*)?$"
    ),
)
_ALIAS_LINE_RE = re.compile(
    (
        r"^(?P<leading_whitespace>\s*)"
        + r"@alias"
        + r"(?P<whitespace_after_directive>\s+)"
        + r"(?P<alias>\S+)"
        + r"(?P<trailing_whitespace>\s*)$"
    ),
)

_STEAM_REFERENCE_RE = re.compile(r"^steam:(?P<value>\d+)$")
_PACKAGE_ID_REFERENCE_RE = re.compile(r"^packageid:(?P<value>\S+)$")
_LOCAL_REFERENCE_RE = re.compile(r"^local:(?P<value>\S+)$")


def parse_modpack_list(text: str) -> ModpackListDocument:
    return ModpackListDocument(
        lines=tuple(
            parse_modpack_list_line(raw_text, line_ending=line_ending)
            for raw_text, line_ending in split_lines_with_endings(text)
        ),
        source_text=text,
    )


def load_modpack_list(path: Path) -> ModpackListDocument:
    return parse_modpack_list(path.read_text(encoding="utf-8"))


def render_modpack_list(document: ModpackListDocument) -> str:
    return "".join(render_modpack_list_line(line) for line in document.lines)


def render_modpack_list_line(line: ModpackListLine) -> str:
    if isinstance(line, BlankLine):
        return f"{line.raw_text}{line.line_ending}"
    if isinstance(line, AliasLine):
        return (
            f"{line.leading_whitespace}"
            f"@alias"
            f"{line.whitespace_after_directive}"
            f"{line.alias}"
            f"{line.trailing_whitespace}"
            f"{line.line_ending}"
        )
    if isinstance(line, CommentLine):
        return f"{line.leading_whitespace}#{line.comment_text}{line.line_ending}"
    if isinstance(line, EntryLine):
        disabled_prefix = "!" if line.disabled else ""
        trailing_comment = (
            f"#{line.trailing_comment_text}"
            if line.trailing_comment_text is not None
            else ""
        )
        return (
            f"{line.leading_whitespace}"
            f"{disabled_prefix}"
            f"{line.whitespace_after_disabled}"
            f"{line.raw_reference_text}"
            f"{line.spacing_before_comment}"
            f"{trailing_comment}"
            f"{line.line_ending}"
        )
    return f"{line.raw_text}{line.line_ending}"


def new_modpack_list_document(
    lines: tuple[ModpackListLine, ...],
) -> ModpackListDocument:
    document = ModpackListDocument(lines=lines)
    return ModpackListDocument(lines=lines, source_text=render_modpack_list(document))


def parse_modpack_list_line(raw_text: str, *, line_ending: str = "") -> ModpackListLine:
    if raw_text.strip() == "":
        return BlankLine(raw_text=raw_text, line_ending=line_ending)

    alias_match = _ALIAS_LINE_RE.fullmatch(raw_text)
    if alias_match is not None:
        return AliasLine(
            raw_text=raw_text,
            leading_whitespace=alias_match.group("leading_whitespace"),
            whitespace_after_directive=alias_match.group("whitespace_after_directive"),
            alias=alias_match.group("alias"),
            trailing_whitespace=alias_match.group("trailing_whitespace"),
            line_ending=line_ending,
        )

    stripped = raw_text.lstrip()
    if stripped.startswith("#"):
        leading_whitespace_length = len(raw_text) - len(stripped)
        return CommentLine(
            raw_text=raw_text,
            leading_whitespace=raw_text[:leading_whitespace_length],
            comment_text=stripped[1:],
            line_ending=line_ending,
        )

    entry_match = _ENTRY_LINE_RE.fullmatch(raw_text)
    if entry_match is None:
        return UnknownLine(raw_text=raw_text, line_ending=line_ending)

    raw_reference_text = entry_match.group("raw_reference_text")
    reference = _parse_reference(raw_reference_text)
    if reference is None:
        return UnknownLine(raw_text=raw_text, line_ending=line_ending)

    disabled_token = entry_match.group("disabled")
    trailing_comment = entry_match.group("trailing_comment")

    return EntryLine(
        raw_text=raw_text,
        leading_whitespace=entry_match.group("leading_whitespace"),
        disabled=disabled_token is not None,
        whitespace_after_disabled=entry_match.group("whitespace_after_disabled"),
        raw_reference_text=raw_reference_text,
        reference=reference,
        spacing_before_comment=entry_match.group("spacing_before_comment"),
        trailing_comment_text=(
            trailing_comment[1:] if trailing_comment is not None else None
        ),
        line_ending=line_ending,
    )


def _parse_reference(raw_reference_text: str) -> ModpackListReference | None:
    steam_match = _STEAM_REFERENCE_RE.fullmatch(raw_reference_text)
    if steam_match is not None:
        return ModpackListReference(kind="steam", value=steam_match.group("value"))

    package_id_match = _PACKAGE_ID_REFERENCE_RE.fullmatch(raw_reference_text)
    if package_id_match is not None:
        return ModpackListReference(
            kind="packageid",
            value=package_id_match.group("value"),
        )

    local_match = _LOCAL_REFERENCE_RE.fullmatch(raw_reference_text)
    if local_match is not None:
        return ModpackListReference(kind="local", value=local_match.group("value"))

    return None
