#!/usr/bin/env python3

import io
import os
import re
import zipfile
from abc import abstractmethod
from filecmp import dircmp
from typing import List, Pattern

import requests
from checksumdir import dirhash
from pathlib import Path

import comission.utilsCMS as uCMS
from comission.utils.logging import LOGGER
from .models.Core import Core
from .models.Addon import Addon
from .models.Alteration import Alteration
from .models.Vulnerability import Vulnerability


class GenericCMS:
    """ Generic CMS object """

    site_url = ""
    release_site = ""
    download_core_url = ""
    base_download_addon_url = ""
    cve_ref_url = ""

    def __init__(self, dir_path, plugins_dir, themes_dir, version="", version_major=""):
        self.dir_path = dir_path
        self.dir_path = dir_path
        self.plugins_dir = plugins_dir
        self.themes_dir = themes_dir
        self.plugins : List[Addon] = []
        self.themes = []
        self.core = Core()
        self.core.version = version
        
        if version == "" and version_major != "":
            self.core.version_major = version_major
        else :
            self.core.version_major = version.split(".")[0]

        self.regex_version_core = re.compile("version = '(.*)';")

        self.core.ignored_files = []
        self.ignored_files_addon: List[str] = []

        self.core_suspect_file_path = ""

    def get_core_version(self) -> str:
        suspects = []

        try:
            with open(os.path.join(self.dir_path, self.core_suspect_file_path)) as version_file:
                for line in version_file:
                    version_core_match = self.regex_version_core.search(line)
                    if version_core_match:
                        suspects.append(version_core_match.group(1).strip())
                        break
        except FileNotFoundError as e:
            LOGGER.debug(str(e))
            pass

        suspects_length = len(suspects)

        if suspects_length == 0:
            LOGGER.print_cms("alert", "[-] Version not found. Search manually !", "", 0)
            return ""

        elif suspects_length == 1:
            LOGGER.print_cms("info", "[+] Version used : " + suspects[0], "", 0)
            self.core.version = suspects[0]
            self.core.version_major = suspects[0].split(".")[0]
            return suspects[0]

        else:
            for suspect in suspects:
                LOGGER.print_cms(
                    "alert",
                    "[-] Multiple versions found." + suspect + " You "
                    "should probably check by yourself manually.",
                    "",
                    0,
                )
            return ""

    def get_addon_version(
        self, addon: Addon, addon_path: str, version_file_regexp: Pattern[str], to_strip: str
    ) -> str:
        version = ""
        try:
            path = os.path.join(addon_path, addon.filename)
            with open(path, encoding="utf8") as addon_info:
                for line in addon_info:
                    version = version_file_regexp.search(line)
                    if version:
                        candidate_version = str(version.group(1).strip(to_strip))
                        if candidate_version != "VERSION": # Drupal specific
                            addon.version = candidate_version
                            LOGGER.print_cms("default", "Version : " + addon.version, "", 1)
                            break

        except FileNotFoundError as e:
            msg = "No standard extension file. Search manually !"
            LOGGER.print_cms("alert", "[-] " + msg, "", 1)
            addon.notes = msg
            return ""
        return addon.version

    @abstractmethod
    def get_url_release(self):
        """
        Get the url to fetch last release data
        """
        pass

    @abstractmethod
    def extract_core_last_version(self, response):
        """
        Extract core last version from HTTP response content
        """
        return ""

    def get_core_last_version(self) -> str:
        """
        Fetch information on last release
        """
        url_release = self.get_url_release()

        try:
            response = requests.get(url_release)
            response.raise_for_status()

            if response.status_code == 200:
                self.last_version = self.extract_core_last_version(response)

        except requests.exceptions.HTTPError as e:
            LOGGER.print_cms("alert", "[-] Unable to retrieve last version. Search manually !", "", 1)
            LOGGER.debug(str(e))
            pass
        return self.last_version

    @abstractmethod
    def get_addon_last_version(self, addon) -> str:
        """
        Get the last released of the plugin and the date
        """
        pass

    @abstractmethod
    def get_archive_name(self) -> str:
        """
        Get the last released of the plugin and the date
        """
        return ""

    def check_core_alteration(self, core_url: str) -> List[Alteration]:
        self.get_archive_name()
        alterations = []
        temp_directory = uCMS.TempDir.create()

        LOGGER.print_cms("info", "[+] Checking core alteration", "", 0)

        try:
            response = requests.get(core_url)
            response.raise_for_status()

            if response.status_code == 200:
                zip_file = zipfile.ZipFile(io.BytesIO(response.content), "r")
                zip_file.extractall(temp_directory)
                zip_file.close()

        except requests.exceptions.HTTPError as e:
            LOGGER.print_cms(
                "alert", "[-] Unable to find the original archive. Search manually !", "", 0
            )
            self.core.alterations = alterations
            LOGGER.debug(str(e))
            return self.core.alterations

        clean_core_path = os.path.join(temp_directory, Path(self.get_archive_name()))

        dcmp = dircmp(clean_core_path, self.dir_path, self.core.ignored_files)
        uCMS.diff_files(dcmp, alterations, self.dir_path) # type: ignore # ignore for "dcmp" variable

        self.core.alterations = alterations
        if alterations is not None:
            msg = "[+] For further analysis, archive downloaded here : " + clean_core_path
            LOGGER.print_cms("info", msg, "", 0)

        return self.core.alterations

    @abstractmethod
    def get_addon_url(self, addon) -> str:
        """
        Generate the addon's url
        """
        pass

    def check_addon_alteration(
        self, addon: Addon, addon_path: str, temp_directory: str
    ) -> str:

        addon_url = self.get_addon_url(addon)

        altered = ""

        try:
            response = requests.get(addon_url)
            response.raise_for_status()

            if response.status_code == 200:
                zip_file = zipfile.ZipFile(io.BytesIO(response.content), "r")
                zip_file.extractall(temp_directory)
                zip_file.close()

                project_dir_hash = dirhash(addon_path, "sha1")
                ref_dir = os.path.join(temp_directory, addon.name)
                ref_dir_hash = dirhash(ref_dir, "sha1")

                if project_dir_hash == ref_dir_hash:
                    altered = "NO"
                    LOGGER.print_cms("good", f"Different from sources : {altered}", "", 1)

                else:
                    altered = "YES"
                    LOGGER.print_cms("alert", f"Different from sources : {altered}", "", 1)

                    dcmp = dircmp(addon_path, ref_dir, self.ignored_files_addon)
                    uCMS.diff_files(dcmp, addon.alterations, addon_path)  # type: ignore # ignore for "dcmp" variable

                addon.altered = altered

                if addon.alterations is not None:
                    LOGGER.print_cms(
                        "info",
                        f"[+] For further analysis, archive downloaded here : {ref_dir}",
                        "",
                        1,
                    )

                LOGGER.print_cms("default", f"To download the addon: {addon_url}", "", 1)

        except requests.exceptions.HTTPError as e:
            addon.notes = "The download link is not standard. Search manually !"
            LOGGER.print_cms("alert", addon.notes, "", 1)
            LOGGER.debug(str(e))
            return addon.notes

        return altered

    @abstractmethod
    def check_vulns_core(self):
        """
        Check if there are any vulns on the CMS core used
        """
        pass

    @abstractmethod
    def check_vulns_addon(self, addon) -> List[Vulnerability]:
        """
        Check if there are any vulns on the plugin
        """
        pass

    def core_analysis(self) -> Core:
        LOGGER.print_cms(
            "info",
            "#######################################################"
            + "\n\t\tCore analysis"
            + "\n#######################################################",
            "",
            0,
        )
        # Check current CMS version
        if self.core.version == "":
            self.get_core_version()

        # Get the last released version
        self.get_core_last_version()

        # Check for vuln on the CMS version
        self.check_vulns_core()

        # Check if the core have been altered
        self.check_core_alteration(self.download_core_url + self.core.version + ".zip")

        return self.core

    @abstractmethod
    def addon_analysis(self, addon_type) -> List[Addon]:
        """
        CMS plugin analysis, return a list of dict
        """
        pass