#!/usr/bin/env python3
import os
import pathlib
import re
import subprocess
from enum import Enum, unique

import click
import yaml

try:
    from latex2mathml.converter import convert as latex_to_mathml
except ImportError:
    latex_to_mathml = None

__version__ = "2.0.0"  # new version

CFG_PATH = pathlib.Path("./config.yml")
STATE_PATH = pathlib.Path("./state.yml")

TC_DIR = pathlib.Path("./_testcases")
TEMPLATES_DIR = pathlib.Path("./templates")
SRC_SPACE_DIR = pathlib.Path("./space")
STORAGE_DIR = pathlib.Path("./storage")
BUILD_DIR = pathlib.Path("./build")


# ======
# Helper Functions
# ======


def _natural_sort_key(s: str):
    """Natural sort key: splits string into text/number chunks for proper ordering."""
    return [int(c) if c.isdigit() else c.lower() for c in re.split(r"(\d+)", s)]


def _parse_range_string(range_str: str) -> list[int]:
    """
    범위 문자열을 정수 리스트로 변환.
    * ..N: 1부터 N까지
    * M..N: M부터 N까지
    * N: 단일 숫자
    """
    if range_str.startswith(".."):
        end_str = range_str[2:]
        if not end_str.isdigit():
            raise ValueError("Invalid range format")
        return list(range(1, int(end_str) + 1))

    parts = range_str.split("..")
    if len(parts) == 1:
        if not parts[0].isdigit():
            raise ValueError("Invalid range format")
        return [int(parts[0])]
    elif len(parts) == 2:
        if not parts[0].isdigit() or not parts[1].isdigit():
            raise ValueError("Invalid range format")
        return list(range(int(parts[0]), int(parts[1]) + 1))
    else:
        raise ValueError("Invalid range format")


def _parse_range_string_list(str_list: list[str]) -> list[int]:
    """
    여러 범위 문자열을 정수 리스트로 변환하는 함수; 이때 중복은 제거함.
        * e.g. ["1..3", "5", "7..9"] -> [1, 2, 3, 5, 7, 8, 9]
    """
    result = set()
    for s in str_list:
        result.update(_parse_range_string(s))
    return list(result)


@unique
class Language(Enum):
    """
    실행 가능한 언어(Language)를 정의하는 클래스입니다. 각 언어는 고유 문자열 값을 가짐.
    - Python: py
    - C: c
    - C++: cpp
    - Rust: rs
    - Kotlin: kt
    - Lua: lua
    """

    PYTHON = "py"
    C = "c"
    CPP = "cpp"
    RUST = "rs"
    KOTLIN = "kt"
    LUA = "lua"

    UNDEFINED = "undefined"

    @property
    def build_command(self):
        """
        각 언어에 대한 빌드 명령어.
        """
        return {
            Language.PYTHON: [
                "python",
                "./make.py",
                "build",
            ],
            Language.C: [
                "make",
                "build",
            ],
            Language.CPP: [
                "make",
                "build",
            ],
            Language.RUST: [
                "make",
                "build",
            ],
            Language.KOTLIN: [
                "./gradlew",
                "jar",
            ],
            Language.LUA: [
                "make",
                "build",
            ],
        }[self]

    @property
    def working_dir(self):
        """
        각 언어의 작업 디렉토리
        """
        return f"{SRC_SPACE_DIR}/src-{self.value}"

    @property
    def build_executable(self):
        """
        각 언어의 빌드 후 생성되는 실행 파일의 이름.
        """
        return {
            Language.PYTHON: "py.pyc",
            Language.C: "c.out",
            Language.CPP: "cpp.out",
            Language.RUST: "rs.out",
            Language.KOTLIN: "kt.jar",
            Language.LUA: "lua.luac",
        }[self]

    @property
    def executable_command(self):
        """
        각 언어에 대한 실행 명령어.
        """
        return {
            Language.PYTHON: ["python", f"{BUILD_DIR}/{self.build_executable}"],
            Language.C: [f"{BUILD_DIR}/{self.build_executable}"],
            Language.CPP: [f"{BUILD_DIR}/{self.build_executable}"],
            Language.RUST: [f"{BUILD_DIR}/{self.build_executable}"],
            Language.KOTLIN: ["java", "-jar", f"{BUILD_DIR}/{self.build_executable}"],
            Language.LUA: ["lua", f"{BUILD_DIR}/{self.build_executable}"],
        }[self]

    @staticmethod
    def convert_ext(ext: str):
        """
        확장자를 언어 Enum으로 변환하는 메소드.
        만약 확장자가 정의된 언어에 해당하지 않으면 UNDEFINED를 반환.
        """
        match ext:
            case "py":
                return Language.PYTHON
            case "c":
                return Language.C
            case "cpp":
                return Language.CPP
            case "rs":
                return Language.RUST
            case "kt":
                return Language.KOTLIN
            case "lua":
                return Language.LUA
            case _:
                return Language.UNDEFINED

    @staticmethod
    def convert_name(format_name: str):
        """
        다양한 언어 형식 이름을 언어 Enum으로 변환하는 메소드.
        convert_ext의 Super Set임.
        """
        if Language.convert_ext(format_name) is not Language.UNDEFINED:
            return Language.convert_ext(format_name)
        match format_name.lower():
            case "py" | "py3" | "python" | "python3":
                return Language.PYTHON
            case "c":
                return Language.C
            case "cpp" | "c++":
                return Language.CPP
            case "rust" | "rs":
                return Language.RUST
            case "kt" | "kotlin":
                return Language.KOTLIN
            case "lua":
                return Language.LUA
            case _:
                return Language.UNDEFINED

    @staticmethod
    def languages() -> list["Language"]:
        return [
            Language.PYTHON,
            Language.C,
            Language.CPP,
            Language.RUST,
            Language.KOTLIN,
            Language.LUA,
        ]


def _dispatch_target(target: str) -> tuple[str, str, Language]:
    splited = target.split("/")
    if len(splited) != 2:
        raise ValueError(
            "Invalid target format. Expected format: <loc>/<problem>.<lang>"
        )

    prob_parts = splited[1].rsplit(".", 1)
    if len(prob_parts) != 2:
        raise ValueError(
            "Invalid target format. Expected format: <loc>/<problem>.<lang>"
        )

    prob_name, ext = prob_parts
    lang = Language.convert_ext(ext)
    if lang is Language.UNDEFINED:
        raise ValueError(f"Unknown language: {ext}")

    return splited[0], prob_name, lang


class StateManager:
    _instance: "StateManager | None" = None
    _state: dict | None = None

    def __new__(cls) -> "StateManager":
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def _load(self) -> dict:
        if self._state is not None:
            return self._state
        try:
            with open(STATE_PATH, "r", encoding="utf-8") as f:
                self._state = yaml.safe_load(f) or {}
        except FileNotFoundError as exc:
            raise click.ClickException(f"state.yml not found at {STATE_PATH}") from exc
        except yaml.YAMLError as e:
            raise click.ClickException(f"state.yml parse error: {e}") from e
        return self._state  # type: ignore[return-value]

    def save(self) -> None:
        if self._state is None:
            return
        with open(STATE_PATH, "w", encoding="utf-8") as f:
            yaml.safe_dump(self._state, f)

    @staticmethod
    def _auto_save(func):
        def wrapper(self, *args, **kwargs):
            result = func(self, *args, **kwargs)
            self.save()
            return result

        return wrapper

    def check_space(self, lang: Language) -> bool:
        state = self._load()
        return lang.value in state.get("space", {}) and bool(state["space"][lang.value])

    def get_space(self, lang: Language) -> dict:
        state = self._load()
        if not self.check_space(lang):
            raise ValueError(f"No space found for language: {lang}")
        return state["space"][lang.value]

    @_auto_save
    def add_space(
        self, lang: Language, file: str, location: str, is_completed: bool
    ) -> None:
        state = self._load()
        if "space" not in state:
            state["space"] = {}
        state["space"][lang.value] = {
            "file": file,
            "location": location,
            "is_completed": is_completed,
        }
        self._state = state

    @_auto_save
    def remove_space(self, lang: Language) -> None:
        state = self._load()
        if "space" in state and lang.value in state["space"]:
            state["space"][lang.value] = {}
            self._state = state

    @_auto_save
    def clear_space(self, lang: Language) -> None:
        state = self._load()
        if "space" in state and lang.value in state["space"]:
            del state["space"][lang.value]
            self._state = state

    def reload(self) -> None:
        self._state = None

    @_auto_save
    def set_completed(self, lang: Language, is_completed: bool) -> None:
        state = self._load()
        if lang.value not in state.get("space", {}):
            raise ValueError(f"No space found for language: {lang}")
        if not state["space"].get(lang.value):
            raise ValueError(f"No space found for language: {lang}")
        state["space"][lang.value]["is_completed"] = is_completed
        self._state = state


class StorageManager:
    _instance: "StorageManager | None" = None

    def __new__(cls) -> "StorageManager":
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def register_location(self, location: str) -> None:
        path = STORAGE_DIR / location
        os.makedirs(path, exist_ok=True)

    def check_location(self, location: str) -> bool:
        path = STORAGE_DIR / location
        return os.path.isdir(path)

    def register_location_lang(self, location: str, lang: Language) -> None:
        path = STORAGE_DIR / location / lang.value
        os.makedirs(path, exist_ok=True)

    def check_location_lang(self, location: str, lang: Language) -> bool:
        path = STORAGE_DIR / location / lang.value
        return os.path.isdir(path)

    def check_get_file(
        self, location: str, lang: Language, filename: str
    ) -> pathlib.Path | None:
        completed_path = (
            STORAGE_DIR
            / location
            / lang.value
            / "completed"
            / f"{filename}.{lang.value}"
        )
        uncompleted_path = (
            STORAGE_DIR / location / lang.value / f"{filename}.{lang.value}"
        )

        if os.path.isfile(completed_path):
            return completed_path
        elif os.path.isfile(uncompleted_path):
            return uncompleted_path
        else:
            return None

    def save_file(
        self,
        location: str,
        lang: Language,
        filename: str,
        content: bytes,
        completed: bool,
    ) -> None:
        path = STORAGE_DIR / location / lang.value / ("completed" if completed else "")
        os.makedirs(path, exist_ok=True)
        with open(path / f"{filename}.{lang.value}", "wb") as f:
            f.write(content)

    def read_file(self, location: str, lang: Language, filename: str) -> bytes:
        path = self.check_get_file(location, lang, filename)
        if path is None:
            raise FileNotFoundError(
                f"File not found in storage: {location}/{filename}.{lang.value}"
            )
        file_path = path / f"{filename}.{lang.value}"
        if not file_path.is_file():
            raise FileNotFoundError(f"File not found in storage: {file_path}")
        with open(file_path, "rb") as f:
            return f.read()

    def remove_file(self, location: str, lang: Language, filename: str) -> None:
        path = self.check_get_file(location, lang, filename)
        if path is None:
            raise FileNotFoundError(
                f"File not found in storage: {location}/{filename}.{lang.value}"
            )
        os.remove(path)

    def mark_completed(self, location: str, lang: Language, filename: str) -> None:
        uncompleted_file = (
            STORAGE_DIR / location / lang.value / f"{filename}.{lang.value}"
        )
        completed_file = (
            STORAGE_DIR
            / location
            / lang.value
            / "completed"
            / f"{filename}.{lang.value}"
        )

        os.makedirs(completed_file.parent, exist_ok=True)
        if os.path.isfile(uncompleted_file):
            os.rename(uncompleted_file, completed_file)

    def unmark_completed(self, location: str, lang: Language, filename: str) -> None:
        uncompleted_file = (
            STORAGE_DIR / location / lang.value / f"{filename}.{lang.value}"
        )
        completed_file = (
            STORAGE_DIR
            / location
            / lang.value
            / "completed"
            / f"{filename}.{lang.value}"
        )

        os.makedirs(uncompleted_file.parent, exist_ok=True)
        if os.path.isfile(completed_file):
            os.rename(completed_file, uncompleted_file)

    @staticmethod
    def construct_target_path(
        location: str, lang: Language, filename: str, completed: bool
    ) -> pathlib.Path:
        return (
            STORAGE_DIR
            / location
            / lang.value
            / ("completed" if completed else "")
            / f"{filename}.{lang.value}"
        )

    def list_entries(
        self,
        location: str | None = None,
        lang: Language | None = None,
        completed: bool | None = None,
    ) -> list[tuple[str, str, str, bool]]:
        """저장된 파일 목록을 반환합니다.

        Args:
            location: location 필터 (None이면 전체)
            lang: language 필터 (None이면 전체)
            completed: completed 필터 (None이면 both, True이면 completed만, False이면 uncompleted만)

        Returns:
            list[tuple[str, str, str, bool]]: (location, lang, filename, is_completed)
        """
        result: list[tuple[str, str, str, bool]] = []

        if not STORAGE_DIR.is_dir():
            return result

        for loc_dir in sorted(STORAGE_DIR.iterdir()):
            if not loc_dir.is_dir():
                continue
            loc_name = loc_dir.name

            if location and loc_name != location:
                continue

            for lang_dir in sorted(loc_dir.iterdir()):
                if not lang_dir.is_dir():
                    continue
                lang_name = lang_dir.name

                if Language.convert_ext(lang_name) is Language.UNDEFINED:
                    continue

                if lang and lang_name != lang.value:
                    continue

                for f in sorted(
                    lang_dir.iterdir(), key=lambda p: _natural_sort_key(p.stem)
                ):
                    if f.is_file():
                        result.append((loc_name, lang_name, f.stem, False))

                completed_dir = lang_dir / "completed"
                if completed_dir.is_dir():
                    for f in sorted(
                        completed_dir.iterdir(), key=lambda p: _natural_sort_key(p.stem)
                    ):
                        if f.is_file():
                            result.append((loc_name, lang_name, f.stem, True))

        if completed is not None:
            result = [e for e in result if e[3] == completed]

        return result


# =======================
# MAIN CLI LOGIC
# =======================


@click.group()
def cli():
    pass


@click.command(name="register")
@click.argument("location", type=str, nargs=1, required=True)
def register(location: str):
    """
    새로운 location을 storage에 등록하는 명령어.
    """
    storage_manager = StorageManager()
    if storage_manager.check_location(location):
        click.secho(
            f"Location '{location}' is already registered.", fg="yellow", bold=True
        )
    else:
        storage_manager.register_location(location)
        click.secho(
            f"Location '{location}' registered successfully.", fg="cyan", bold=True
        )


@click.command(name="create")
@click.argument("target", type=str, nargs=1, required=True)
@click.option(
    "--completed/--no-completed", "-c/-nc", default=False, help="Mark as completed"
)
@click.option(
    "--no-template",
    "-nt",
    is_flag=True,
    help="Do not use template content (create empty file instead)",
)
@click.option(
    "--force", "-f", is_flag=True, help="Overwrite existing file if it already exists"
)
def create(target: str, completed: bool, no_template: bool, force: bool):
    """
    지정된 location을 storage에 생성하는 명령어
    """
    loc, prob, lang = _dispatch_target(target)

    if not StorageManager().check_location(loc):
        raise click.ClickException(
            f"Location '{loc}' is not registered. Please register it first."
        )

    filename = prob
    existing = StorageManager().check_get_file(loc, lang, filename)
    if existing:
        click.secho(
            f"File '{filename}.{lang.value}' already exists in location '{loc}'",
            fg="yellow",
            bold=True,
        )
        if not force:
            return

    if no_template:
        content = b""
    else:
        template_path = TEMPLATES_DIR / f"template.{lang.value}"
        if template_path.is_file():
            content = template_path.read_bytes()
            click.secho(f"Using template: {template_path}", fg="cyan")
        else:
            content = b""
            click.secho(f"Template not found: {template_path}", fg="yellow")

    StorageManager().save_file(
        loc, lang, filename, content, completed
    )
    click.secho(
        f"Created '{filename}.{lang.value}' in location '{loc}'",
        fg="cyan",
        bold=True,
    )


@click.command(name="run")
@click.argument("lang", type=str, nargs=1, required=True)
@click.option("--testcase", "-t", default=["1"], multiple=True)
@click.option("--verbose/--no-verbose", "-v/-nv", default=True)
def run(lang: str, testcase: tuple[str, ...], verbose: bool):
    """
    지정된 언어로 빌드 및 실행하는 명령어
    """

    # Language 확인
    target_language: Language = Language.convert_name(lang)

    if target_language is Language.UNDEFINED:
        raise click.ClickException("Undefined Language Exception")

    # load된 state가 있는지 확인
    if not StateManager().check_space(target_language):
        raise click.ClickException(
            f"No loaded file for language '{target_language.value}'. Please load a file first."
        )

    # 테케 분석
    tcs: list[int] = _parse_range_string_list(list(testcase))

    # build
    try:
        s = subprocess.run(
            target_language.build_command,
            check=True,
            capture_output=True,
            text=True,
            cwd=target_language.working_dir,
        )
        print(s.stderr)

    except subprocess.CalledProcessError as e:
        click.secho(">>>>>> [BUILD TIME ERROR] >>>>>>", fg="red", bold=True)
        raise click.ClickException(e.stderr)

    # execution for each test case
    for tc in tcs:
        if not (
            os.path.isfile(f"{TC_DIR}/{tc}.in") and os.path.isfile(f"{TC_DIR}/{tc}.out")
        ):
            click.echo(f"{tc:02d}: {None}")
        else:
            f_in_path = f"{TC_DIR}/{tc}.in"
            f_out_path = f"{TC_DIR}/{tc}.out"

            with open(f_out_path, "r", encoding="utf-8") as f_out:
                expected = f_out.read().strip()

            f_in = open(f_in_path, "r", encoding="utf-8")
            proc = subprocess.Popen(
                target_language.executable_command,
                stdin=f_in,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )

            if not verbose:
                for line in proc.stdout:  # type: ignore
                    print(line, end="")

            stdout, stderr = proc.communicate()
            f_in.close()

            if proc.returncode != 0:
                click.secho(">>>>>> [RUN TIME ERROR] >>>>>>", fg="red", bold=True)
                click.secho(f"{stderr}", fg="red")
                click.secho(f"returncode: {proc.returncode}", fg="red")
                continue

            flag = stdout.strip() == expected
            symbol = (
                click.style("✓", fg="green") if flag else click.style("✗", fg="red")
            )
            click.echo(f"{tc:02d}: {symbol}")

            if verbose:
                click.secho("===" * 3, fg="bright_black")
                click.secho("[INPUT]", fg="cyan", bold=True)
                with open(f_in_path, "r") as f:
                    click.echo(f.read())
                click.secho("[ACTUAL]", fg="cyan", bold=True)
                click.echo(stdout.strip() if stdout else "(empty)")
                click.secho("===" * 3, fg="bright_black")


@click.command(name="load")
@click.argument("target", type=str, nargs=1, required=True)
@click.option("--force", "-f", is_flag=True)
def load(target: str, force: bool):
    """
    Storage의 파일을 각 언어의 src-space로 이동시키는 명령.
    - force는 src-space에 이미 파일이 존재할 때, 덮어쓸지 결정
    """

    # 파일의 src-space가 어딘지 확인
    loc, prob, lang = _dispatch_target(target)

    if StateManager().check_space(lang):
        if not force:
            raise click.ClickException(
                f"Space for language '{lang.value}' is already occupied."
            )

    path = StorageManager().check_get_file(loc, lang, prob)

    if path is None:
        raise click.ClickException(
            f"File '{prob}.{lang.value}' not found in location '{loc}'. Use 'create' command to create it first."
        )

    dest_path = SRC_SPACE_DIR / f"src-{lang.value}" / "src" / f"main.{lang.value}"

    with open(path, "rb") as f_src, open(dest_path, "wb") as f_dst:
        f_dst.write(f_src.read())
    StateManager().remove_space(lang)
    StateManager().add_space(lang, prob, loc, False)
    StorageManager().remove_file(loc, lang, prob)
    click.secho(
        f"Loaded '{prob}.{lang.value}' from location '{loc}' to src-space.",
        fg="cyan",
        bold=True,
    )


@click.command(name="export")
@click.argument("from_", type=str, nargs=-1)
@click.option(
    "--all", "all_", default=False, is_flag=True, help="Export all files in the space"
)
@click.option(
    "--force",
    "-f",
    is_flag=True,
    help="Overwrite existing file in storage if it already exists",
)
def export(from_: tuple[str, ...], all_: bool, force: bool):
    """
    space에 있는 파일을 storage의 각 location으로 이동시키는 명령.
    """

    def _export_sub_command(from_language: Language):
        space = StateManager().get_space(from_language)

        s_file, s_loc, s_is_completed = (
            space["file"],
            space["location"],
            space["is_completed"],
        )

        path = StorageManager.construct_target_path(
            s_loc, from_language, s_file, s_is_completed
        )

        if path.is_file() and not force:
            raise click.ClickException(
                f"File '{path.name}' already exists in location '{s_loc}'. Use --force to overwrite."
            )

        src_path = (
            SRC_SPACE_DIR
            / f"src-{from_language.value}"
            / "src"
            / f"main.{from_language.value}"
        )

        with open(src_path, "rb") as f_src:
            content = f_src.read()
        os.remove(src_path)
        StorageManager().save_file(
            s_loc, from_language, s_file, content, s_is_completed
        )
        StateManager().clear_space(from_language)
        click.secho(
            f"Exported '{s_file}.{from_language.value}' to location '{s_loc}'",
            fg="cyan",
            bold=True,
        )

    if all_:
        for l in Language.languages():
            if StateManager().check_space(l):
                _export_sub_command(l)
    else:
        froms = map(lambda x: Language.convert_name(x), from_)
        for l in froms:
            if not StateManager().check_space(l):
                click.secho(
                    f"No loaded file for language '{l.value}' in space. Skipping export for this language.",
                    fg="yellow",
                )
                continue
            _export_sub_command(l)


@click.group(name="state")
def state():
    """State management commands"""
    pass


@state.command(name="show")
def state_show():
    """Show current space state"""
    click.secho("Current Space State:", fg="cyan", bold=True)
    for lang in Language.languages():
        space = (
            StateManager().get_space(lang) if StateManager().check_space(lang) else None
        )
        if space:
            status_symbol = (
                click.style("✓", fg="green")
                if space.get("is_completed")
                else click.style("○", fg="white")
            )
            click.echo(
                f"{status_symbol} {lang.value}: {space['file']} (Location: {space['location']})"
            )
        else:
            click.echo(f"{click.style('○', fg='white')} {lang.value}: No file loaded")


@state.command(name="complete")
@click.argument("lang", type=str)
def state_complete(lang: str):
    """Mark language space as completed"""
    lang_obj = Language.convert_name(lang)
    if lang_obj is Language.UNDEFINED:
        raise click.ClickException(f"Unknown language: {lang}")

    state_manager = StateManager()
    try:
        state_manager.set_completed(lang_obj, True)
        state_manager.save()
    except ValueError as e:
        raise click.ClickException(str(e))
    click.secho(f"Language '{lang}' marked as completed.", fg="green")


@state.command(name="uncomplete")
@click.argument("lang", type=str)
def state_uncomplete(lang: str):
    """Mark language space as not completed"""
    lang_obj = Language.convert_name(lang)
    if lang_obj is Language.UNDEFINED:
        raise click.ClickException(f"Unknown language: {lang}")

    state_manager = StateManager()
    try:
        state_manager.set_completed(lang_obj, False)
        state_manager.save()
    except ValueError as e:
        raise click.ClickException(str(e))
    click.secho(f"Language '{lang}' marked as not completed.", fg="green")


@click.command(name="init")
@click.argument("count", type=int)
def init(count: int):
    """
    init testcases with initial count and state.yml and config.yml
    """
    if count < 1:
        raise click.UsageError("count should be greater than 0")

    TC_DIR.mkdir(exist_ok=True)
    for i in range(1, count + 1):
        (TC_DIR / f"{i}.in").touch()
        (TC_DIR / f"{i}.out").touch()
    click.secho(f"1. Testcases created with count {count}", fg="cyan")
    click.secho("2. Checking state.yml...", fg="cyan")
    if not STATE_PATH.is_file():
        click.secho("2. state.yml does not exist, creating...", fg="yellow")
        STATE_PATH.write_text("space:\n", encoding="utf-8")
        click.secho("2. state.yml created", fg="cyan")
    else:
        click.secho("2. state.yml exists", fg="green")


@click.command(name="show")
@click.argument("filter_", type=str, default=None, required=False)
@click.option("--completed", "completed_mode", flag_value="completed", default=True)
@click.option("--uncompleted", "completed_mode", flag_value="uncompleted")
@click.option("--both-completed", "completed_mode", flag_value="both")
def show(filter_: str | None, completed_mode: str):
    """Show all stored problems in storage directory.

    FILTER format (optional):
      location        e.g. zeta
      location/lang   e.g. zeta/rs
      /lang           e.g. /py

    FILTER MODE (default: --completed):
      --completed       show completed only
      --uncompleted     show uncompleted only
      --both-completed  show both completed and uncompleted
    """
    completed = {"completed": True, "uncompleted": False, "both": None}[completed_mode]

    location = lang = None
    if filter_:
        parts = filter_.split("/", 1)
        location = parts[0] if parts[0].strip() else None
        lang = parts[1] if len(parts) == 2 else None

    entries = StorageManager().list_entries(
        location=location,
        lang=Language.convert_name(lang) if lang else None,
        completed=completed,
    )

    if not entries:
        click.echo(" - ")
        return

    total = len(entries)
    completed_count = sum(1 for e in entries if e[3])
    uncompleted_count = total - completed_count

    click.secho(
        f"Total: {total} (completed: {completed_count}, uncompleted: {uncompleted_count})",
        fg="cyan",
        bold=True,
    )
    click.echo()

    current_loc = None
    current_lang = None
    for loc_name, lang_name, file_name, is_completed in entries:
        if loc_name != current_loc:
            current_loc = loc_name
            current_lang = None
            click.secho(f"[{loc_name}]", fg="green", bold=True)

        if lang_name != current_lang:
            current_lang = lang_name
            click.secho(f"  {lang_name}/", fg="yellow")

        status = (
            click.style("✓", fg="green")
            if is_completed
            else click.style("○", fg="white")
        )
        click.echo(f"    {status} {file_name}.{lang_name}")


@click.command(name="find")
@click.argument("filter_", type=str)
@click.option(
    "--completed/--no-completed",
    "-c/-nc",
    default=None,
    help="Filter by completed status (default: all)",
)
def find(filter_: str, completed: bool | None):
    """
    Find problems by filter in storage.

    FILTER format:
      filter                e.g. 2447
      filter.ext            e.g. 2447.py  (filters by language)
      .ext                  e.g. .py      (all files of a language)
      location/filter       e.g. zeta/2447
      location/filter.ext    e.g. zeta/2447.py
      location/.ext          e.g. zeta/.py
    """
    location: str | None = None
    filter_base: str | None = None
    lang: Language | None = None

    if "/" in filter_:
        parts = filter_.split("/", 1)
        location = parts[0] or None
        filter_ = parts[1]
    else:
        location = None

    if "." in filter_:
        filter_base, ext = filter_.rsplit(".", 1)
        lang_candidate = Language.convert_ext(ext)
        if lang_candidate is not Language.UNDEFINED:
            lang = lang_candidate
            filter_base = None if filter_base == "" else filter_base
        else:
            filter_base = filter_
    else:
        filter_base = filter_

    entries = StorageManager().list_entries(
        location=location, lang=lang, completed=completed
    )
    if filter_base is not None:
        entries = [e for e in entries if filter_base.lower() in e[2].lower()]
    entries.sort(key=lambda e: (e[0], e[1], _natural_sort_key(e[2])))

    if not entries:
        click.echo("No problems found.")
        return

    total_count = len(entries)
    completed_count = sum(1 for e in entries if e[3])
    uncompleted_count = sum(1 for e in entries if not e[3])

    click.secho(
        f"Found: {total_count} (completed: {completed_count}, uncompleted: {uncompleted_count})",
        fg="cyan",
        bold=True,
    )
    click.echo()

    current_loc = None
    current_lang = None
    for loc_name, lang_name, file_name, is_completed in entries:
        if loc_name != current_loc:
            current_loc = loc_name
            current_lang = None
            click.secho(f"[{loc_name}]", fg="green", bold=True)

        if lang_name != current_lang:
            current_lang = lang_name
            click.secho(f"  {lang_name}/", fg="yellow")

        status = (
            click.style("✓", fg="green")
            if is_completed
            else click.style("○", fg="white")
        )
        click.echo(f"    {status} {file_name}.{lang_name}")


@click.command(name="version")
def version():
    """Show version information."""
    click.echo(f"CodeObject version {__version__}")


@click.command(name="status")
def status():
    """Show storage statistics."""
    all_entries = StorageManager().list_entries()

    if not all_entries:
        click.echo("No problems in storage.")
        return

    stats: dict[str, dict[str, dict[str, int]]] = {}
    for loc, lang, _, is_completed in all_entries:
        if loc not in stats:
            stats[loc] = {}
        if lang not in stats[loc]:
            stats[loc][lang] = {"total": 0, "completed": 0}
        stats[loc][lang]["total"] += 1
        if is_completed:
            stats[loc][lang]["completed"] += 1

    total_problems = len(all_entries)
    total_completed = sum(1 for _, _, _, c in all_entries if c)

    click.secho("Storage Status", fg="cyan", bold=True)
    click.echo()

    for loc_name, langs in sorted(stats.items()):
        loc_total = sum(c["total"] for c in langs.values())
        loc_completed = sum(c["completed"] for c in langs.values())
        loc_uncompleted = loc_total - loc_completed
        click.secho(f"[{loc_name}]", fg="green", bold=True, nl=False)
        click.secho(f" {loc_total}", fg="yellow", bold=False, nl=False)
        click.secho(" (", fg="white", bold=False, nl=False)
        click.secho(f"{loc_completed}", fg="green", bold=False, nl=False)
        click.secho(" completed, ", fg="cyan", bold=False, nl=False)
        click.secho(f"{loc_uncompleted}", fg="red", bold=False, nl=False)
        click.secho(" not completed", fg="cyan", bold=False, nl=False)
        click.secho(")", fg="white", bold=False)
        for lang_name, counts in sorted(langs.items()):
            click.echo(f"  {lang_name}: {counts['completed']}/{counts['total']}")
        click.echo()

    click.secho(
        f"Total: {total_problems} (completed: {total_completed}, "
        f"uncompleted: {total_problems - total_completed})",
        fg="cyan",
    )


@click.command(name="fetchprob")
@click.argument("target", type=str, nargs=1, required=True)
@click.option("--force", "-f", is_flag=True, help="Overwrite existing static files")
def fetchprob(target: str, force: bool):
    """(disabled) Fetch BOJ problem HTML - BOJ is closing, keeping for future multi-location support."""
    del target, force
    raise click.ClickException("fetchprob is currently disabled (BOJ closing)")


cli.add_command(register)
cli.add_command(create)
cli.add_command(run)
cli.add_command(load)
cli.add_command(init)
cli.add_command(export)
cli.add_command(state)
cli.add_command(show)
cli.add_command(find)
cli.add_command(version)
cli.add_command(status)
cli.add_command(fetchprob)

if __name__ == "__main__":
    cli()
