"""Rename file level suffixes.

Usage: filelvl [OPTIONS] TARGETS...
"""

from __future__ import annotations

import argparse
import concurrent.futures
import logging
import os
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Final

# Configuration constants
LEVELS: Final[dict[str, str]] = {
    "0": "",
    "1": "PUB,NOR",
    "2": "INT",
    "3": "CON",
    "4": "CLA",
}
BRACKETS: Final[list[str]] = [" ([_（【-", " )]_）】"]
MARK_BRACKETS: Final[list[str]] = ["(", ")"]

logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)


@dataclass
class FileLevelConfig:
    """File level configuration."""

    levels: dict[str, str] = None
    brackets: list[str] = None
    mark_brackets: list[str] = None

    def __post_init__(self) -> None:
        """Initialize configuration with default values."""
        if self.levels is None:
            self.levels = LEVELS.copy()
        if self.brackets is None:
            self.brackets = BRACKETS.copy()
        if self.mark_brackets is None:
            self.mark_brackets = MARK_BRACKETS.copy()


@dataclass
class FileProcessor:
    """File processor for level renaming."""

    src: Path
    filestem: str
    config: FileLevelConfig

    def rename(self, level: int = 0) -> None:
        """Rename file with specified level."""
        # Remove all file level marks.
        for level_names in self.config.levels.values():
            if level_names:  # Skip empty level (level 0)
                self._remove_marks(marks=level_names.split(","))
        logger.info(f"After remove level marks: {self.filestem}")

        # Remove all digital marks.
        self._remove_marks(marks=list("".join([str(x) for x in range(1, 10)])))
        logger.info(f"After remove digital marks: {self.filestem}")

        # Add level mark.
        self._add_level_mark(level=level)
        logger.info(f"After add level mark: {self.filestem}")

        # Rename file
        target_path = self.src.with_name(self.filestem + self.src.suffix)
        logger.info(f"Rename: {self.src}->{target_path}")
        self.src.rename(target_path)

    def _add_level_mark(self, level: int) -> None:
        """Add level mark to filename, must be 1-4."""
        levelstr = self.config.levels.setdefault(str(level), "").split(",")[0]
        if not levelstr:
            logger.warning(f"Invalid level: {level}, skip.")
            return

        suffix = levelstr.join(self.config.mark_brackets)
        self.filestem = f"{self.filestem}{suffix}"
        if self.filestem == self.src.stem:
            logger.error(f"{self.filestem} equals to original, skip.")
            return

        dst_path = self.src.with_name(self.filestem + self.src.suffix)
        if dst_path.exists():
            logger.warning(
                f"{dst_path.name} already exists, add unique id.",
            )
            self.filestem += str(uuid.uuid4()).join(self.config.mark_brackets)
            self._add_level_mark(level)

    def _remove_marks(self, marks: list[str]) -> None:
        """Remove marks from filename."""
        for mark in marks:
            self.filestem = self._remove_mark(self.filestem, mark, self.config.brackets)

    @staticmethod
    def _remove_mark(stem: str, mark: str, brackets: list[str]) -> str:
        """Remove mark from filename.

        Args:
            stem: Filename stem
            mark: Mark to remove
            brackets: Bracket characters for validation

        Returns:
            str: Filename without the mark.
        """
        pos = stem.find(mark)
        if pos == -1:
            logger.debug(f"{mark} not found in: {stem}.")
            return stem

        b, e = pos - 1, pos + len(mark)
        if b >= 0 and e <= len(stem) - 1:
            if stem[b] not in brackets[0] or stem[e] not in brackets[1]:
                return stem[:e] + FileProcessor._remove_mark(stem[e:], mark, brackets)
            stem = stem.replace(stem[b : e + 1], "")
            return FileProcessor._remove_mark(stem, mark, brackets)
        return stem


class ParallelRunner:
    """Run file processing tasks in parallel."""

    def __init__(self, max_workers: int = 8) -> None:
        """Initialize parallel runner.

        Args:
            max_workers: Maximum number of concurrent workers
        """
        self.max_workers = max_workers

    def run(self, func, targets: list[FileProcessor]) -> None:
        """Run function on targets in parallel.

        Args:
            func: Function to apply to each target
            targets: List of FileProcessor instances
        """
        if not targets:
            logger.error("No valid targets to process.")
            return

        with concurrent.futures.ThreadPoolExecutor(
            max_workers=self.max_workers
        ) as executor:
            executor.map(func, targets)


def process_files(targets: list[Path], level: int = 0) -> None:
    """Process multiple files with specified level.

    Args:
        targets: List of target file paths
        level: File level (0-4)

    Raises:
        ValueError: If level is not in valid range
    """
    # Parameter validation
    if level < 0 or level > 4:
        logger.error(f"Invalid level {level}, must be between 0 and 4")
        raise ValueError("Level must be between 0 and 4")

    if not targets:
        logger.error("No target files specified")
        raise ValueError("At least one target file is required")

    # Validate all target files exist and are writable
    valid_targets = []
    for target in targets:
        if not target.exists():
            logger.warning(f"File does not exist: {target}")
            continue
        if not target.is_file():
            logger.warning(f"Not a file: {target}")
            continue
        if not os.access(target, os.W_OK):
            logger.warning(f"File not writable: {target}")
            continue
        valid_targets.append(target)

    if not valid_targets:
        logger.error("No valid target files found")
        raise ValueError("No valid target files found")

    config = FileLevelConfig()
    rename_targets = [FileProcessor(t, t.stem, config) for t in valid_targets]
    runner = ParallelRunner()
    runner.run(lambda processor: processor.rename(level=level), rename_targets)


def main() -> None:
    """Main entry point for file level processing."""
    parser = argparse.ArgumentParser(
        description="Rename file level suffixes",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  filelevel file1.txt file2.txt --level 1
  filelevel *.txt -l 2
  filelevel document.pdf --level 0  # Clear level
        """,
    )
    parser.add_argument(
        "targets",
        nargs="+",
        type=Path,
        help="Target files to process",
    )
    parser.add_argument(
        "--level",
        "-l",
        type=int,
        default=0,
        choices=[0, 1, 2, 3, 4],
        help="File level (0-4), 0 to clear level (default: 0)",
    )

    args = parser.parse_args()
    process_files(args.targets, args.level)


if __name__ == "__main__":
    main()
