"""Clean build folders, such as .venv, node_modules, dist, build, etc."""

from __future__ import annotations

import argparse
import logging
import os
import shutil
import stat
import time
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
from typing import Final

# Folders to clean
_BUILD_FOLDERS: Final[tuple[str, ...]] = ("build", "dist", ".venv", "node_modules")


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

CWD = Path.cwd()
PROJECT_ROOT = Path(__file__).parent.parent.parent


@dataclass(frozen=True)
class SearchResult:
    """Search result for build folders."""

    root_dir: Path = field(default_factory=Path)
    folders_to_delete: list[Path] = field(default_factory=list)
    folders_to_skip: list[Path] = field(default_factory=list)

    def __repr__(self) -> str:
        """Return a string representation of the SearchResult."""
        return f"<SearchResult: root={self.root_dir}>, delete={self.delete_counter}, skip={self.skip_counter}\n>"

    @cached_property
    def delete_counter(self) -> Counter:
        """Get the most common folder names."""
        return Counter([folder.name for folder in self.folders_to_delete])

    @cached_property
    def skip_counter(self) -> Counter:
        """Get the most common folder names."""
        return Counter([folder.name for folder in self.folders_to_skip])

    @cached_property
    def delete_count(self) -> int:
        """Get the total number of folders to delete."""
        return len(self.folders_to_delete)

    def perform_delete(self) -> None:
        """Perform the delete operation."""
        t0 = time.perf_counter()
        deleted_count = 0
        failed_folders = []
        with ThreadPoolExecutor(max_workers=4) as executor:
            future_to_folder = {
                executor.submit(SearchResult.delete_folder, folder): folder for folder in self.folders_to_delete
            }
            for future in as_completed(future_to_folder):
                folder, error = future.result()
                if error:
                    logger.error(f"Error deleting {folder}: {error}")
                    failed_folders.append(folder)
                else:
                    logger.info(f"Deleted {folder}")
                    deleted_count += 1

        if failed_folders:
            logger.warning(f"Failed to delete {len(failed_folders)} folders")
            return

        logger.info(f"Successfully deleted {deleted_count}/{self.delete_count} folders")
        logger.info(f"Time taken: {time.perf_counter() - t0:.4f}s")

    @staticmethod
    def handle_remove_readonly(func, path, _) -> None:
        """Handle Windows readonly files when removing."""
        try:
            Path(path).chmod(stat.S_IWUSR)
            func(path)
        except OSError:
            if not Path(path).exists():
                pass
            else:
                raise

    @staticmethod
    def delete_folder(folder: Path):
        """Delete a single folder with error handling."""
        try:
            shutil.rmtree(folder, onerror=SearchResult.handle_remove_readonly)
            return folder, None
        except Exception as e:
            return folder, e

    @staticmethod
    def is_target_folder(entry: Path) -> bool:
        """Check if a path is a build folder."""
        return entry.is_dir() and entry.name in _BUILD_FOLDERS

    @staticmethod
    def from_path(directory: Path) -> SearchResult:
        """Create a SearchResult from a path."""
        logger.info(f"Searching for build folders in {directory}")

        folders_to_delete = []
        folders_to_skip = []
        for root, dirs, _ in os.walk(directory, onerror=lambda _: None):
            root_path = Path(root)
            for dir_name in dirs:
                entry = root_path / dir_name
                if entry == PROJECT_ROOT / ".venv":
                    folders_to_skip.append(entry)
                    continue
                if SearchResult.is_target_folder(entry):
                    folders_to_delete.append(entry)

        result = SearchResult(
            root_dir=directory,
            folders_to_delete=folders_to_delete,
            folders_to_skip=folders_to_skip,
        )
        logger.info(f"Found {result.delete_count} build folders to delete")
        logger.info(f"Summary: {result}")
        return result


def parse_args():
    """Parse command-line arguments."""
    parser = argparse.ArgumentParser(description="Clean build folders")
    parser.add_argument(
        "directory",
        type=str,
        nargs="?",
        default=str(CWD),
        help="Directory to clean",
    )
    parser.add_argument("--yes", "-y", action="store_true", help="Skip confirmation")
    return parser.parse_args()


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

    Cli utility tool

    Examples
    --------
      cli [options] <arguments>
    """
    args = parse_args()

    result = SearchResult.from_path(Path(args.directory))
    if not args.yes:
        logger.info(
            f"\nAre you sure you want to clean build folders in {args.directory}? (y/n) [n]",
        )
        choice = input().lower()
        if choice != "y":
            logger.info("Dry run, no changes will be made")
            return

    logger.info("Running clean build")
    result.perform_delete()


if __name__ == "__main__":
    main()
