###
# Copyright 2024 Hewlett Packard Enterprise, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###

# -*- coding: utf-8 -*-
"""Base implementation for interaction with vnic interface"""

from redfish.rest.connections import ChifDriverMissingOrNotFound, VnicNotEnabledError
from redfish.rest.v1 import InvalidCredentialsError
import logging
from ctypes import (
    POINTER,
    byref,
    c_bool,
    c_char_p,
    c_int,
    c_uint8,
    c_wchar_p,
    create_string_buffer,
)

from redfish.hpilo.risblobstore2 import BlobStore2
from redfish.hpilo.rishpilo import BlobReturnCodes


# ---------End of imports---------
# ---------Debug logger---------

LOGGER = logging.getLogger(__name__)


class GenerateAndSaveAccountError(Exception):
    """Raised when errors occured while generating and saving app account"""

    pass


class ReactivateAppAccountTokenError(Exception):
    """Raised when errors occurred while reactivating app account"""

    pass

class InactiveAppAccountTokenError(Exception):
    """Raised when inactive app account token"""

    pass


class RemoveAccountError(Exception):
    """Raised when errors occured while removing app account"""

    pass


class AppAccountExistsError(Exception):
    """Raised when errors occured while removing app account"""

    pass


class VnicLoginError(Exception):
    """Raised when error occurs while VNIC login"""

    pass


class AppIdListError(Exception):
    """Raised when error occurs during CompareAppIds"""

    pass


class SavinginTPMError(Exception):
    """Raised when error occurs while saving app account in TPM"""

    pass


class SavinginiLOError(Exception):
    """Raised when error occurs while saving app account in iLO"""

    pass


class GenBeforeLoginError(Exception):
    """Raised when error occurs while getting iLO Gen before login"""

    pass


class InvalidCommandLineError(Exception):
    """Raised when invalid command line arguments are passed"""

    pass


class AppAccount(object):
    def __init__(self, appname=None, appid=None, salt=None, username=None, password=None, log_dir=None):
        """Initialize the AppAccount object and configure logging."""
        self.appname = (appname or "self_register").encode("utf-8")
        self.dll = BlobStore2.gethprestchifhandle()
        self.salt = (salt or "self_register").encode("utf-8")
        self.username = username
        self.password = password
        self.log_dir = log_dir

        LOGGER.debug(
            f"Initialization parameters - AppName: {self.appname.decode('utf-8')}, "
            f"Salt: {self.salt.decode('utf-8')}, Username: {self.username if self.username else 'None'}"
        )

        if log_dir and LOGGER.isEnabledFor(logging.DEBUG):
            logdir_c = create_string_buffer(log_dir.encode("utf-8"))
            self.dll.enabledebugoutput(logdir_c)
            LOGGER.info("Debug output enabled. Logs stored in: %s", log_dir)

        if appid and len(appid) == 4:
            appid = self.ExpandAppId(appid)

        self.appid = (appid or "self_register").encode("utf-8")

    def ExpandAppId(self, appid):
        """Calls the DLL function to expand a 4-character app ID."""
        self.dll.ExpandAppid.argtypes = [c_char_p, POINTER(c_char_p)]
        self.dll.ExpandAppid.restype = c_int

        host_app_id_c = c_char_p(appid.encode("utf-8"))
        expanded_app_id_c = c_char_p(None)

        LOGGER.debug(f"Calling ExpandAppid with AppID: {appid}")
        ret = self.dll.ExpandAppid(host_app_id_c, byref(expanded_app_id_c))
        if ret:
            LOGGER.error(f"Failed to expand App ID {appid} (Return code: {ret})")
            raise AppAccountExistsError()

        expanded_id = expanded_app_id_c.value.decode("utf-8") if expanded_app_id_c else None
        if LOGGER.isEnabledFor(logging.DEBUG):
            LOGGER.debug(f"Successfully expanded App ID {appid} to {expanded_id}")
        return expanded_id

    def generate_and_save_apptoken(self):
        """Generate and save an application account, logging each step."""
        LOGGER.info("Starting generate_and_save_apptoken process.")

        self.dll.GenerateAppToken.argtypes = [c_char_p, c_char_p, c_char_p, c_char_p, c_char_p]
        self.dll.GenerateAppToken.restype = c_int

        self.username = self.username.encode("utf-8")
        self.password = self.password.encode("utf-8")

        LOGGER.debug("Calling GenerateAppToken for AppID: %s", self.appid.decode("utf-8"))

        returncode = self.dll.GenerateAppToken(self.appid, self.appname, self.salt, self.username, self.password)

        if returncode == BlobReturnCodes.SUCCESS:
            LOGGER.info("Application account successfully generated and saved.")
            return BlobReturnCodes.SUCCESS
        elif returncode == -54:
            LOGGER.error("Failed to save account in TPM.")
            raise SavinginTPMError()
        elif returncode == -53:
            LOGGER.error("Failed to save account in iLO.")
            raise SavinginiLOError()
        elif returncode == -55:
            LOGGER.warning("Application account already exists.")
            raise AppAccountExistsError()
        elif returncode == -8:
            raise InvalidCredentialsError(0)
        else:
            LOGGER.critical("Unknown return code: %d", returncode)
            raise GenerateAndSaveAccountError()

    def reactivate_apptoken(self):
        """Reactivate an application account, logging each step."""
        LOGGER.info("Starting reactivate_apptoken process.")

        self.dll.ReactivateAppToken.argtypes = [c_char_p, c_char_p, c_char_p, c_char_p, c_char_p]
        self.dll.ReactivateAppToken.restype = c_int

        user_name = self.username.encode("utf-8")
        password = self.password.encode("utf-8")

        LOGGER.debug("Calling ReactivateAppToken for AppID: %s", self.appid.decode("utf-8"))

        return_code = self.dll.ReactivateAppToken(self.appid, self.appname, self.salt, user_name, password)

        if return_code == BlobReturnCodes.SUCCESS:
            LOGGER.info("Application account has been reactivated successfully.")
            return BlobReturnCodes.SUCCESS
        elif return_code == -8:
            LOGGER.error("Please enter valid credentials.")
            raise InvalidCredentialsError(0)
        else:
            LOGGER.critical("Unknown return code: %d", return_code)
            raise ReactivateAppAccountTokenError()

    def remove_apptoken(self):
        returncode = 1
        if self.username and self.password:
            LOGGER.debug(f"Using credentials to remove for AppID: {self.appid.decode('utf-8')}")
            self.dll.DeleteAppTokenUsingCreds.argtypes = [c_char_p, c_char_p, c_char_p]
            self.dll.DeleteAppTokenUsingCreds.restype = c_int
            self.username = self.username.encode("utf-8")
            self.password = self.password.encode("utf-8")
            returncode = self.dll.DeleteAppTokenUsingCreds(self.appid, self.username, self.password)

        elif self.appname and self.salt:
            LOGGER.debug(f"Using app details to remove for AppID : {self.appid.decode('utf-8')}")
            self.dll.DeleteAppToken.argtypes = [c_char_p, c_char_p, c_char_p]
            self.dll.DeleteAppToken.restype = c_int
            returncode = self.dll.DeleteAppToken(self.appid, self.appname, self.salt)

        LOGGER.debug(f"Delete operation returned code: {returncode}")

        if returncode == BlobReturnCodes.SUCCESS:
            return BlobReturnCodes.SUCCESS
        elif returncode == -8:
            raise InvalidCredentialsError(0)
        else:
            raise RemoveAccountError("Error occurred while removing app account.")

    def apptoken_exists(self):
        """Check if an application account exists in TPM."""
        LOGGER.info("Checking if application account exists.")

        self.dll.AppIdExistsinTPM.argtypes = [c_char_p, POINTER(c_bool)]
        self.dll.AppIdExistsinTPM.restype = c_int

        vexists = c_bool(False)
        LOGGER.debug(f"Checking existence for AppID: {self.appid.decode('utf-8')}")
        returncode = self.dll.AppIdExistsinTPM(self.appid, byref(vexists))
        LOGGER.debug(f"AppIdExistsinTPM returned: {returncode}")

        if returncode == BlobReturnCodes.SUCCESS:
            LOGGER.info("app account existence check completed. Exists: %s", vexists.value)
            return vexists.value
        else:
            LOGGER.error("Failed to check account existence")
            LOGGER.debug(f"Failed with return code: {returncode}")
            raise AppAccountExistsError()

    def vlogin(self):
        LOGGER.info("Starting VNIC login process.")

        self.dll.GetSessionID.argtypes = [c_char_p, c_char_p, c_char_p, POINTER(c_char_p), POINTER(c_char_p)]
        self.dll.GetSessionID.restype = c_int
        session_id = c_char_p()
        session_location = c_char_p()
        errorcode = 0

        LOGGER.debug(f"Attempting login for AppID: {self.appid.decode('utf-8')}")
        try:
            errorcode = self.dll.GetSessionID(
                self.appid, self.appname, self.salt, byref(session_id), byref(session_location)
            )
            LOGGER.debug(f"GetSessionID returned code: {errorcode}")
            if errorcode == 2:
                raise InactiveAppAccountTokenError()
            elif errorcode != BlobReturnCodes.SUCCESS:
                raise Exception
            session_id = session_id.value.decode()
            session_location = session_location.value.decode()
            session_location = "/" + session_location.split("/", 3)[-1]
            LOGGER.debug("Successfully decoded session information")
            LOGGER.info(
                "VNIC login successful. Session Location: %s, Session ID: ****%s", session_location, session_id[-4:]
            )

            return session_id, session_location
        except InactiveAppAccountTokenError:
            LOGGER.error("Login failed due to an inactive or expired App Account token.")
            raise InactiveAppAccountTokenError()
        except Exception as excp:
            LOGGER.error("VNIC login failed: %s", str(excp))
            raise VnicLoginError()

    def vnic_exists(self):
        """Check if the VNIC is ready for use."""
        LOGGER.info("Checking if VNIC is ready for use.")

        self.dll.ReadyToUse.restype = c_int
        LOGGER.debug("Calling ReadyToUse")
        status = self.dll.ReadyToUse()
        LOGGER.debug(f"ReadyToUse returned status: {status}")

        if status == BlobReturnCodes.SUCCESS:
            LOGGER.info("VNIC is ready for use.")
            return True
        else:
            LOGGER.warning("VNIC is not ready for use.")
            return False

    def version_beforelogin(self):
        """Detect the iLO version before login."""
        LOGGER.info("Detecting iLO version before login.")

        self.dll.DetectILO.argtypes = [c_char_p, POINTER(c_int), POINTER(c_int)]
        self.dll.DetectILO.restype = c_int

        ilo_type = c_int(0)
        security_state = c_int(0)

        if not self.appid:
            self.appid = "self_register".encode("utf-8")

        returncode = self.dll.DetectILO(self.appid, byref(ilo_type), byref(security_state))
        LOGGER.debug(f"DetectILO returned: type={ilo_type.value}, security={security_state.value}, code={returncode}")

        if returncode == BlobReturnCodes.SUCCESS:
            LOGGER.info(
                "iLO version successfully detected: %d, Security State: %d", ilo_type.value, security_state.value
            )
            return ilo_type.value, security_state.value
        elif returncode == -1 and ilo_type.value == 100:
            LOGGER.info(
                "Detectilo returned ilotype: %d, Security State: %d, " "returncode: %d",
                ilo_type.value,
                security_state.value,
                returncode,
            )
            raise ChifDriverMissingOrNotFound()
        elif returncode == -50 and ilo_type.value == 101:
            LOGGER.info(
                "Detectilo returned ilotype: %d, Security State: %d, " "returncode: %d",
                ilo_type.value,
                security_state.value,
                returncode,
            )
            raise VnicNotEnabledError()
        elif returncode == -2:
            LOGGER.info("Invalid parameters were passed in the command line. Detectilo iLO returncode: -2")
            raise InvalidCommandLineError()
        else:
            LOGGER.info(
                "Detectilo returned ilotype: %d, Security State: %d, " "returncode: %d",
                ilo_type.value,
                security_state.value,
                returncode,
            )
            LOGGER.error("Failed to detect iLO version.")
            raise GenBeforeLoginError()

    def GetIPAddress(self):
        """Retrieve the IP address of the iLO interface."""
        LOGGER.info("Retrieving IP address.")

        LOGGER.debug("Calling GetIPAddress")
        self.dll.GetIPAddress.restype = c_wchar_p
        ip_addr = self.dll.GetIPAddress()
        LOGGER.debug(f"GetIPAddress returned: {ip_addr if ip_addr else 'None'}")

        LOGGER.info("IP address retrieved: %s", ip_addr)
        return ip_addr

    def CompareAppIds(self):
        """Compare application IDs and return a structured dictionary."""
        LOGGER.info("Starting CompareAppIds process.")

        self.dll.CompareAppIds.argtypes = [
            c_char_p,
            c_char_p,
            c_char_p,
            POINTER(POINTER(c_char_p)),
            POINTER(POINTER(c_char_p)),
            POINTER(c_int),
            POINTER(POINTER(c_uint8)),
            POINTER(POINTER(c_uint8)),
        ]
        self.dll.CompareAppIds.restype = c_int

        app_ids_ptr = POINTER(c_char_p)()
        app_names_ptr = POINTER(c_char_p)()
        app_id_count = c_int()
        in_tpm_ptr = POINTER(c_uint8)()
        in_ilo_ptr = POINTER(c_uint8)()

        LOGGER.debug(
            "Calling CompareAppIds with AppID: %s, AppName: %s",
            self.appid.decode("utf-8"),
            self.appname.decode("utf-8"),
        )

        returncode = self.dll.CompareAppIds(
            self.appid,
            self.appname,
            self.salt,
            byref(app_ids_ptr),
            byref(app_names_ptr),
            byref(app_id_count),
            byref(in_tpm_ptr),
            byref(in_ilo_ptr),
        )
        if returncode != BlobReturnCodes.SUCCESS:
            LOGGER.error("Error occurred in CompareAppIds. Return Code: %d", returncode)
            raise AppIdListError("Error occurred while retrieving App Id information.\n")

        LOGGER.info("CompareAppIds executed successfully. Number of App IDs: %d", app_id_count.value)
        app_ids = [app_ids_ptr[i].decode("utf-8") for i in range(app_id_count.value)]
        app_names = [app_names_ptr[i].decode("utf-8") for i in range(app_id_count.value)]
        in_tpm_list = [bool(in_tpm_ptr[i]) for i in range(app_id_count.value)]
        in_ilo_list = [bool(in_ilo_ptr[i]) for i in range(app_id_count.value)]

        appid_dict = list()
        for i in range(app_id_count.value):
            appid_dict.append(
                {
                    "ApplicationID": app_ids[i],
                    "ApplicationName": app_names[i],
                    "ExistsInTPM": in_tpm_list[i],
                    "ExistsIniLO": in_ilo_list[i],
                }
            )

        LOGGER.info("CompareAppIds completed successfully. Processed %d AppIDs.", len(appid_dict))

        return appid_dict
