import pandas as pd
import numpy as np
from rebulk.match import MatchesDict
import logging
from guessit import guessit
from pathlib import Path
import re
import sys
import shutil


class FilmProcessor:
    def __init__(self, input_folders: list[str], output_folder: str):
        self.file_extensions: list[str] = ["mp4", "avi", "mkv", "mpeg", "m2ts"]
        self.input_folders: list[str] = input_folders
        self.output_folder: Path = Path(output_folder)
        self.film_list_df_types: dict[str, str] = {
            "Full file path": "string",
            "Parsed film title": "string",
            "Film size (GB)": "float64",
            "Resolution": "string",
            "Codec": "string",
            "Source": "string",
            "Release group": "string",
            "Already on ANT?": "string",
            "Info": "string",
        }
        self.csv_file_path: Path = self.output_folder / "Film list.csv"
        self.backup_csv_file_path: Path = (
            self.output_folder / "Film list old version backup.csv"
        )

    def get_film_file_paths(self) -> list[Path]:
        """
        Scan the input folder from parameters for the given file extensions,
        adding any files to a list.
        """
        paths = []
        for ext in self.file_extensions:
            for folder in self.input_folders:
                paths.extend(Path(folder).glob(f"**/*.{ext}"))

        filtered_paths = self.remove_paths_containing_extras_folder(paths)
        cleaned_paths = self.remove_paths_if_unopenable(filtered_paths)

        if not cleaned_paths:
            raise ValueError(
                "No films were found, check the INPUT_FOLDERS value in parameters.py"
            )

        cleaned_paths.sort()

        return cleaned_paths

    def get_film_info_from_file_paths(
        self, film_file_paths: list[Path]
    ) -> pd.DataFrame:
        film_sizes = self.get_film_sizes_from_file_paths(film_file_paths)
        guessed_films = self.get_guessit_info_from_film_paths(film_file_paths)
        film_titles = self.get_formatted_titles_from_guessed_films(guessed_films)
        film_resolutions = self.get_film_resolutions_from_guessed_films(guessed_films)
        film_codecs = self.get_codec_from_guessed_films(guessed_films)
        film_sources = self.get_source_from_guessed_films(guessed_films)
        release_groups = self.get_release_groups_from_guessed_films(guessed_films)

        film_list_df = self.create_film_list_dataframe(
            film_file_paths,
            film_sizes,
            film_titles,
            film_resolutions,
            film_codecs,
            film_sources,
            release_groups,
        )

        return film_list_df

    def combine_with_existing_film_csv(
        self, film_list_df: pd.DataFrame
    ) -> pd.DataFrame:
        should_read_csv = self.check_if_existing_film_csv_exists()

        if not should_read_csv:
            return film_list_df

        existing_film_list = pd.read_csv(self.csv_file_path)

        should_combine_film_lists = self.check_if_existing_csv_is_compatible(
            existing_film_list
        )

        if should_combine_film_lists:
            combined_film_list = self.combine_current_film_list_with_existing_csv(
                existing_film_list, film_list_df
            )

            return combined_film_list

        return film_list_df

    def combine_current_film_list_with_existing_csv(
        self, existing_film_list: pd.DataFrame, current_film_list: pd.DataFrame
    ) -> pd.DataFrame:

        logging.info(
            "Combining existing output file with current list of films "
            "and dropping duplicate film titles..."
        )
        existing_film_list_formatted = existing_film_list.astype(
            self.film_list_df_types
        )

        combined_film_list = (
            pd.concat([current_film_list, existing_film_list_formatted])
            .drop_duplicates(subset=["Parsed film title"], keep="last")
            .reset_index(drop=True)
            .fillna("")  # Fill NAs with empty strings for later dupe handling
        )

        self.stop_process_if_all_films_already_in_existing_csv(combined_film_list)

        return combined_film_list

    def stop_process_if_all_films_already_in_existing_csv(
        self, combined_film_list_df: pd.DataFrame
    ) -> None:
        if all(
            combined_film_list_df["Already on ANT?"].str.contains(
                r"^Duplicate:", regex=True, na=False
            )
        ):
            logging.info(
                "All films have already been searched and are duplicates. "
                "\n\nEnding the process early.\n\n----"
            )
            sys.exit(0)

    def remove_paths_containing_extras_folder(
        self, file_paths: list[Path]
    ) -> list[Path]:
        """
        Remove any file file paths containing Extras as their parent folder.
        """
        cleaned_paths = [path for path in file_paths if path.parent.name != "Extras"]

        return cleaned_paths

    def get_film_sizes_from_file_paths(self, file_paths: list[Path]) -> list[float]:
        film_sizes = [self.get_file_size_from_path(path) for path in file_paths]

        return film_sizes

    def remove_paths_if_unopenable(self, paths):
        """
        If file does not exist or is not openable, remove from paths.
        Warn user if path exceeds 260 characters.
        """
        cleaned_paths = []
        for path in paths:
            if not path.is_file():
                file_name = path.stem
                warning_message = (
                    f"{file_name} could not be opened or does not exist, skipping."
                )
                if len(str(path)) > 260:
                    warning_message += " This may be caused by a file path exceeding 260 characters. Try shortening the folder or file name."
                logging.warning(warning_message)
            else:
                cleaned_paths.append(path)

        return cleaned_paths

    def get_file_size_from_path(self, file_path: Path) -> float:
        file_size_in_bytes = file_path.stat().st_size
        file_size = self.convert_bytes_to_gb(file_size_in_bytes)

        return file_size

    def convert_bytes_to_gb(self, num_in_bytes: int) -> float:
        """
        Convert bytes to correct unit of measurement as a string
        """
        num_in_gb = round((num_in_bytes / 1073741824), 2)

        return num_in_gb

    def get_guessit_info_from_film_paths(
        self, file_paths: list[Path]
    ) -> list[MatchesDict]:
        """
        Use guessit package to extract film information
        into ordered dictionary.
        """
        logging.info("Using Guessit to extract film information, may take a while...")
        guessed_media = [guessit(path) for path in file_paths]
        guessed_films = [media for media in guessed_media if media.get("type") == "movie"]

        logging.info("Finished using Guessit to extract film information")

        return guessed_films

    def get_formatted_titles_from_guessed_films(
        self, guessed_films: list[MatchesDict]
    ) -> list[str]:
        """
        Get film titles from guessit objects, then fix titles missing
        full stops within acronyms
        """
        titles = [
            self.get_film_attribute_from_guessed_film(film, "title")
            for film in guessed_films
        ]
        cleaned_titles = [self.fix_title_if_contains_acronym(title) for title in titles]

        return cleaned_titles

    def get_film_attribute_from_guessed_film(
        self, guessed_film: MatchesDict, attribute: str
    ) -> str:
        """
        Extract the given guessit attribute from a given guessit object.
        """
        try:
            film_attribute = guessed_film[attribute]
            if isinstance(film_attribute, list):
                film_attribute = ", ".join(film_attribute)
        except:
            film_attribute = ""

        return film_attribute

    def get_film_resolutions_from_guessed_films(
        self, guessed_films: list[MatchesDict]
    ) -> list[str]:
        film_resolutions = [
            self.get_film_attribute_from_guessed_film(film, "screen_size")
            for film in guessed_films
        ]

        return film_resolutions

    def get_codec_from_guessed_films(
        self, guessed_films: list[MatchesDict]
    ) -> list[str]:
        film_codecs = [
            self.get_film_attribute_from_guessed_film(film, "video_codec")
            for film in guessed_films
        ]

        film_codecs_cleaned = [re.sub(r"\.", "", codec) for codec in film_codecs]

        return film_codecs_cleaned

    def get_source_from_guessed_films(
        self, guessed_films: list[MatchesDict]
    ) -> list[str]:
        film_sources = [
            self.get_film_attribute_from_guessed_film(film, "source")
            for film in guessed_films
        ]

        film_sources_cleaned = [
            re.sub("Ultra HD Blu-ray", "Blu-ray", source) for source in film_sources
        ]

        return film_sources_cleaned

    def get_release_groups_from_guessed_films(
        self, guessed_films: list[MatchesDict]
    ) -> list[str]:
        release_groups = [
            self.get_film_attribute_from_guessed_film(film, "release_group").lower()
            for film in guessed_films
        ]

        return release_groups

    def fix_title_if_contains_acronym(self, film_title: str) -> str:
        """
        After guessit has extracted film title, fix instances where
        consecutive single letter words contain spaces instead of full stops.
        e.g. L A Confidential -> L.A Confidential -> L.A. Confidential
        e.g. S W A T -> S.W.A.T -> S.W.A.T.
        """
        acronym_spaces_as_full_stops = re.sub(
            r"(?<=\b[A-Za-z]{1})\s(?=[A-Za-z]{1}\b)", ".", film_title
        )

        acronym_suffixed_with_a_full_stop = re.sub(
            r"(?<=\.[A-Za-z])(\s|$)(?=[^\s])", ". ", acronym_spaces_as_full_stops
        )

        acronym_at_end_of_title_suffixed_with_full_stop = re.sub(
            r"(?<=\..$)",
            ".",
            acronym_suffixed_with_a_full_stop,
        )

        return acronym_at_end_of_title_suffixed_with_full_stop

    def create_film_list_dataframe(
        self,
        film_file_paths: list[Path],
        film_sizes: list[float],
        film_titles: list[str],
        film_resolutions: list[str],
        film_codecs: list[str],
        film_sources: list[str],
        film_release_groups: list[str],
    ) -> pd.DataFrame:
        """
        Combine the full file paths and film titles into a
        pandas DataFrame.
        Add empty ANT check column before we attempt to combine an existing
        film list csv, in case one does not exist.
        """
        films_df = pd.DataFrame(
            {
                "Full file path": film_file_paths,
                "Parsed film title": film_titles,
                "Film size (GB)": film_sizes,
                "Resolution": film_resolutions,
                "Codec": film_codecs,
                "Source": film_sources,
                "Release group": film_release_groups,
                "Already on ANT?": np.repeat("", len(film_file_paths)),
                "Info": np.repeat("", len(film_file_paths)),
            }
        ).astype(self.film_list_df_types)

        return films_df

    def check_if_existing_film_csv_exists(self) -> bool:
        if not self.csv_file_path.is_file():
            logging.info(
                "An existing output file (%s) was not found, processing films "
                "from scratch...",
                self.csv_file_path,
            )
            return False

        logging.info("An existing output file (%s) was found.", self.csv_file_path)

        return True

    def check_if_existing_csv_is_compatible(
        self, existing_film_df: pd.DataFrame
    ) -> bool:
        existing_columns = list(existing_film_df.columns)

        if existing_columns != list(self.film_list_df_types.keys()):
            logging.warning(
                "Warning: existing file was created using an old version of ANT upload checker.\n"
                "-------- Existing file is being skipped as it doesn't contain all the required columns.\n"
                "-------- Creating a backup film list from the previous version (%s).\n"
                "-------- This can be deleted if you don't need it.\n",
                self.backup_csv_file_path,
            )

            shutil.copy(self.csv_file_path, self.backup_csv_file_path)

            return False

        return True
