"""Model for the File object."""
import difflib
import re
from datetime import datetime
from pathlib import Path
from typing import Any

import rich.repr

from jdfile._config import Config
from jdfile.models.dates import Date
from jdfile.models.project import Project
from jdfile.utils import alerts
from jdfile.utils.alerts import logger as log
from jdfile.utils.enums import Separator
from jdfile.utils.nltk import find_synonyms
from jdfile.utils.questions import select_folder
from jdfile.utils.strings import (
    insert,
    match_case,
    normalize_separators,
    split_camelcase_words,
    split_words,
    strip_special_chars,
    strip_stopwords,
    transform_case,
)


@rich.repr.auto
class File:
    """Representation for a File object."""

    def __init__(
        self, path: Path = None, config: Config = Config(), project: Project = None
    ) -> None:
        """Initialize the File object.

        Args:
            path (Path, optional): Path to the file. Defaults to None.
            config (Config, optional): Configuration object. Defaults to Config().
            project (Project, optional): Project object. Defaults to None.
        """
        self.config = config
        self.accessed = datetime.fromtimestamp(path.stat().st_atime)
        self.created = datetime.fromtimestamp(path.stat().st_ctime)
        self.modified = datetime.fromtimestamp(path.stat().st_mtime)
        self.name = path.name
        self.parent = path.parent
        self.path = path.expanduser().resolve()
        self.suffixes = self.path.suffixes
        self.stem = str(self.path)[: str(self.path).rfind("".join(self.path.suffixes))].replace(
            f"{str(self.parent)}/", ""
        )
        if re.match(r"^\.", self.stem):
            self.is_dotfile: bool = True
        else:
            self.is_dotfile = False
        self.project = project
        self.skip_file = False

        #### Create new attributes for the file ####
        if self.config.date_format:
            self.date = Date(date_format=config.date_format, string=self.stem, ctime=self.created)
        else:
            self.date = None

        if self.config.clean:
            self.new_stem = self._clean_stem()
            self.new_suffixes = self._clean_suffixes()
        else:
            self.new_stem = self.stem
            if self.date and self.date.found_string:
                self.new_stem = re.sub(re.escape(self.date.found_string), "", self.new_stem)
            if self.date and self.date.reformatted_date:
                self.new_stem = insert(
                    self.new_stem,
                    self.date.reformatted_date,
                    self.config.insert_location,
                    self.config.separator,
                )
            self.new_suffixes = self.suffixes.copy()

        self.new_parent = (
            self._get_new_parent()
            if self.project and self.project.exists and config.organize
            else self.parent
        )

    def __rich_repr__(self) -> rich.repr.RichReprResult:  # pragma: no cover
        """Rich representation of the File object.

        Returns:
            rich.repr.RichReprResult: Rich representation of the File object.
        """
        yield "path", self.path
        yield "name", self.name
        yield "stem", self.stem
        yield "parent", self.parent
        yield "suffixes", self.suffixes
        yield "created", self.created
        yield "modified", self.modified
        yield "accessed", self.accessed
        yield "is_dotfile", self.is_dotfile
        yield "target", self.target

    def _clean_stem(self) -> str:
        """Create a cleaned stem for the file.

        Returns:
            str: Cleaned stem for the file.
        """
        new_stem = self.stem

        if self.date and self.date.found_string:
            new_stem = re.sub(re.escape(self.date.found_string), "", new_stem)

        if self.config.split_words:
            new_stem = split_camelcase_words(new_stem, self.config.match_case)

        if self.config.strip_stopwords:
            new_stem = strip_stopwords(new_stem, self.config.stopwords)

        new_stem = strip_special_chars(new_stem)
        new_stem = transform_case(new_stem, self.config.transform_case)
        new_stem = match_case(new_stem, self.config.match_case)
        new_stem = normalize_separators(new_stem, self.config.separator)
        new_stem = new_stem.strip(" -_.")

        if self.date and self.date.reformatted_date:
            new_stem = insert(
                new_stem,
                self.date.reformatted_date,
                self.config.insert_location,
                self.config.separator,
            )

        if self.is_dotfile and not re.match(r"^\.", new_stem):
            return f".{new_stem}"

        return new_stem

    def _clean_suffixes(self) -> list[str]:
        """Clean suffixes from the new filename.

        Returns:
            list[str]: Cleaned suffixes.
        """
        return [
            ".jpg" if ext == ".jpeg" or ext == ".JPEG" else ext.lower() for ext in self.suffixes
        ]

    def _get_new_parent(self) -> Path:
        """Identify the new parent directory for the file.

        Returns:
            Path: New directory for the file.
        """
        words_in_stem = self.tokenize_stem()

        # If a JD number is specified, stop matching other terms
        if match_from_number := [
            i.path for i in self.project.usable_folders if i.number in words_in_stem
        ]:
            log.trace(f"'{self.name}' matched folder from number")
            return match_from_number[0]

        # Match terms to folders
        possible_folders: dict[str, Any] = {}
        for f in self.project.usable_folders:
            key = str(f.path.relative_to(self.project.path))
            for term in [_t.lower() for _t in f.terms]:
                if term.lower() in words_in_stem and key not in possible_folders:
                    possible_folders[key] = [f, [term]]
                if term.lower() in words_in_stem and key in possible_folders:
                    possible_folders[key][1].append(term)

        if len(possible_folders) == 0:
            log.warning(f"Skipping '{self.name}': No matching folders")
            self.skip_file = True
            return self.parent

        if len(possible_folders) == 1 or self.config.force:
            log.trace(f"'{self.name}': 1 matching folder")
            return list(possible_folders.values())[0][0].path

        if len(possible_folders) > 1:
            log.trace(f"'{self.name}': {len(possible_folders)} matching folders")
            result = select_folder(
                possible_folders=possible_folders,
                project_path=self.project.path,
                filename=self.name,
            )
            if result == "skip":
                self.skip_file = True
                return self.parent

            return Path(result)

        # If no matches are found, skip the file
        self.skip_file = True
        return self.parent

    @property
    def target(self) -> Path:
        """Return the target path for the file.

        Returns:
            Path: Target path for the file.
        """
        return Path(self.new_parent / f"{self.new_stem}{''.join(self.new_suffixes)}")

    def commit(self) -> bool:
        """Commit the changes to the file by writing the new filename to disk.

        Returns:
            bool: True if the file was successfully moved, False otherwise.
        """
        if not self.has_changes() or self.skip_file:
            log.info(f"{self.name} -> No changes")
            return False

        if self.project and self.project.exists:
            try:
                display = ".../" + str(self.target.relative_to(self.project.path.parents[0]))
            except ValueError:  # pragma: no cover
                display = str(self.target)
        else:
            display = self.target.name

        if self.target.exists() and self.config.overwrite_existing and not self.target.is_dir():
            if self.config.dry_run:
                alerts.dryrun(f"{self.name} -> {display}")
                return True

            self.path.rename(self.target)
            log.success(f"{self.name} -> {display}")
            return True

        self.unique_name()
        if self.config.dry_run:
            alerts.dryrun(f"{self.name} -> {display}")
            return True
        self.path.rename(self.target)
        log.success(f"{self.name} -> {display}")
        return True

    def has_changes(self) -> bool:
        """Return whether the file has a change.

        Returns:
            True if the file has a change, False otherwise.
        """
        return self.target != self.path

    def print_diff(self) -> str:
        """Print the diff between the original and new stems.

        Returns:
            str: Diff between the original and new stems.
        """
        output = []
        a = f"{self.stem}{''.join(self.suffixes)}"
        b = f"{self.new_stem}{''.join(self.new_suffixes)}"

        matcher = difflib.SequenceMatcher(None, a, b)

        green = "[green reverse]"
        red = "[red reverse]"
        endgreen = "[/]"
        endred = "[/]"

        for opcode, a0, a1, b0, b1 in matcher.get_opcodes():
            if opcode == "equal":
                output.append(a[a0:a1])
            elif opcode == "insert":
                output.append(f"{green}{b[b0:b1]}{endgreen}")
            elif opcode == "delete":
                output.append(f"{red}{a[a0:a1]}{endred}")
            elif opcode == "replace":
                output.append(f"{green}{b[b0:b1]}{endgreen}")
                output.append(f"{red}{a[a0:a1]}{endred}")

        return "".join(output)

    def tokenize_stem(self) -> list[str]:
        """Tokenize the stem of the file.

        Returns:
            list[str]: List of tokens from the stem, all lowercase.
        """
        string = self.new_stem
        string = split_camelcase_words(string)
        words_in_stem = split_words(string)
        words_in_stem.extend(self.config.cli_terms)
        synonyms = []
        if self.config.use_nltk:  # pragma: no cover
            for _w in words_in_stem:
                synonyms.extend(find_synonyms(_w))
        words_in_stem.extend(synonyms)

        return sorted({x.lower() for x in words_in_stem})

    def unique_name(self) -> None:
        """Append an integer to the end of the new filename if the target path already exists."""
        match self.config.separator:
            case Separator.UNDERSCORE:
                sep = "_"
            case Separator.DASH:
                sep = "-"
            case Separator.SPACE:
                sep = " "
            case Separator.NONE:
                sep = ""
            case _:
                sep = "_"

        ########################################
        i = 1
        original_new_stem = f"{self.new_stem}"
        while self.target.exists():
            self.new_stem = f"{original_new_stem}{sep}{i}"
            i += 1
