"""Rename file level suffixes."""

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

from pytola_core.config import ConfigMixin

__version__ = "0.1.5"
__build__ = "20260303"

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


class FileLevelConfig(ConfigMixin):
    """File level configuration."""

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


conf = FileLevelConfig()


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

    Attributes
    ----------
    src : Path
        Source file path
    config : FileLevelConfig
        Configuration for file processing
    _filestem : str | None
        Cached filestem without extension and level marks
    logger : logging.Logger
        Logger instance for the processor
    """

    src: Path
    config: FileLevelConfig
    _filestem: str | None = None

    @property
    def filestem(self) -> str:
        """Get filestem without level marks."""
        if self._filestem is None:
            self._filestem = self.src.stem
        return self._filestem

    @filestem.setter
    def filestem(self, value: str) -> None:
        """Set filestem value."""
        self._filestem = value

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

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

        # Add level mark.
        self._add_level_mark(level=level)
        logger.debug(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 = conf.LEVELS.setdefault(str(level), "").split(",")[0]
        if not levelstr:
            logger.warning(f"Invalid level: {level}, skip.")
            return

        suffix = levelstr.join(conf.MARK_BRACKETS)
        self.filestem = f"{self.filestem}{suffix}"
        if self.filestem == self.src.stem:
            logger.warning(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(conf.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, conf.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


def process_file(target: Path, level: int = 0) -> None:
    """Process a single file with specified level.

    Args:
        target: Target file path
        level: File level (0-4)
    """
    if level < 0 or level > conf.MAX_LEVELS - 1:
        logger.error(f"Invalid level {level}, must be between 0 and {conf.MAX_LEVELS - 1}")
        return

    if not target.exists():
        logger.error(f"File does not exist: {target}")
        return

    if not os.access(target, os.W_OK):
        logger.error(f"File not writable: {target}")
        return

    config = FileLevelConfig()
    processor = FileProcessor(target, config)
    processor.rename(level=level)


def parse_args() -> argparse.Namespace:
    """Parse command line arguments.

    Returns
    -------
        argparse.Namespace: Parsed namespace containing:
            - targets (list[Path]): Files to process
            - level (int): File level 0-4 (default: 0)
            - max_workers (int): Thread pool size (default: 4)
            - debug (bool): Enable debug logging

    Examples
    --------
    >>> parse_args(['file.txt', '-l', '2'])
    Namespace(targets=[PosixPath('file.txt')], level=2, ...)
    """
    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)",
    )
    parser.add_argument(
        "--max-workers",
        "-w",
        type=int,
        default=4,
        help="Maximum number of worker threads for parallel processing (default: 4)",
    )
    parser.add_argument("--debug", "-d", action="store_true", help="Enable debug mode")

    return parser.parse_args()


def main() -> None:
    """Run entry point for the command-line interface."""
    args = parse_args()

    if args.debug:
        logger.setLevel(logging.DEBUG)

    if not any(t.is_file() for t in args.targets):
        logger.error(f"No valid target files found in: {args.targets}")
        return

    if len(args.targets) >= conf.CONCURRENT_MIN_COUNT:
        logger.info("Using parallel processing for large number of files")
        with concurrent.futures.ThreadPoolExecutor(max_workers=args.max_workers) as executor:
            futures = [executor.submit(process_file, target, args.level) for target in args.targets]
            for future in concurrent.futures.as_completed(futures):
                try:
                    future.result()  # Check for exceptions
                except Exception:  # noqa: PERF203
                    logger.exception("Error processing file")
    else:
        logger.info("Using serial processing for small number of files")
        for target in args.targets:
            try:
                process_file(target, args.level)
            except Exception:  # noqa: PERF203
                logger.exception(f"Error processing file {target}")


if __name__ == "__main__":
    main()
