import os.path
import re
import subprocess
import shutil

import ogGlobals
import SystemLib
import FileSystemLib
import DiskLib
import FileLib
import InventoryLib

#!/bin/bash
# Libreria provisional para uso de UEFI
# Las funciones se incluirán las librerías ya existentes

#/**
#         ogNvramActiveEntry
#@brief   Activa entrada de la NVRAM identificada por la etiqueta o el orden
#@param    Num_order_entry | Label_entry Número de orden o la etiqueta de la entrada a borrar.
#@return  (nada)
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTUEFI   UEFI no activa.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#*/ ##
#ogNvramActiveEntry ('2')
#ogNvramActiveEntry ('Windows Boot Manager')
def ogNvramActiveEntry (entry):
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    numentries = []
    efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
    try:
        foo = int (entry, 16)     ## raises ValueError if entry doesn't look like hexadecimal
        num = f'{foo:04x}'.upper()
        for l in efibootmgr_out.splitlines():
            words = l.split()
            if len(words) < 2: continue
            if num in words[0]:
                numentries.append (words[0][4:8])
    except ValueError:
        for l in efibootmgr_out.splitlines():
            words = l.split (maxsplit=2)
            if len(words) < 3: continue
            if words[1] == entry:
                numentries.append (words[0][4:8])

    if not numentries:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'NVRAM entry "{entry}"')
        return

    if 1 != len(numentries):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_GENERIC, f'more than one entry found')
        return

    subprocess.run (['efibootmgr', '-a', '-b', numentries[0]], capture_output=True, text=True)

#/**
#         ogNvramAddEntry
#@brief   Crea nueva entrada en el gestor de arranque (NVRAM), opcionalmente la incluye al final del orden de arranque.
#@param    Str_Label_entry Número de disco o etiqueta de la entrada a crear.
#@param    Str_BootLoader  Número de partición o cargador de arranque.
#@param    Bool_Incluir_Arranque  Incluir en el orden de arranque (por defecto FALSE) (opcional)
#@return  (nada)
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTUEFI   UEFI no activa.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#*/ ##
#ogNvramAddEntry ('1', '2', True)
#ogNvramAddEntry ('grub', '/EFI/grub/grubx64.efi', True)
#ogNvramAddEntry ('Windows', '/EFI/Microsoft/Boot/bootmgfw.efi')
def ogNvramAddEntry (bootlbl, bootldr, nvram_set=False):
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    esp = DiskLib.ogGetEsp()
    if not esp:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, 'ESP')
        return
    efidisk, efipar = esp.split()

    try:
        foo = int(bootlbl) + int(bootldr)   ## raises ValueError if bootlbl/bootldr don't look like numbers
        bootlabel = f'Part-{int(bootlbl):02d}-{int(bootldr):02d}'
        bootloader = f'/EFI/{bootlabel}/Boot/ogloader.efi'
    except ValueError:
        bootlabel = bootlbl
        bootloader = bootldr

    ogNvramDeleteEntry (bootlabel)

    dev = DiskLib.ogDiskToDev (efidisk)
    subprocess.run (['efibootmgr', '-C', '-d', dev, '-p', efipar, '-L', bootlabel, '-l', bootloader])

    if nvram_set:
        efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
        for l in efibootmgr_out.splitlines():
            if '\t' not in l: continue
            (pre_tab, post_tab) = l.split ('\t', maxsplit=1)
            words = pre_tab.split (maxsplit=1)
            if len(words) < 2: continue     ## shouldn't happen
            if words[1] == bootlabel:
                numentry = words[0][4:8]
        order = ogNvramGetOrder()
        ogNvramSetOrder (order + [numentry])



#/**
#         ogCopyEfiBootLoader int_ndisk str_repo path_image
#@brief   Copia el cargador de arranque desde la partición EFI a la de sistema.
#@param   int_ndisk    nº de orden del disco
#@param   int_part     nº de partición
#@return  (nada, por determinar)
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#@note    Si existe el cargador en la partición de sistema no es válido
#*/ ##
def ogCopyEfiBootLoader (disk, par):
    mntdir = FileSystemLib.ogMount (disk, par)
    if not mntdir:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, f'{disk} {par}')
        return None

    esp = DiskLib.ogGetEsp()
    if not esp:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, 'EFI partition')
        return
    esp_disk, esp_par = esp.split()
    efidir = FileSystemLib.ogMount (esp_disk, esp_par)
    if not efidir:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, 'ESP')
        return

    bootlabel = f'Part-{int(disk):02d}-{int(par):02d}'
    osversion = InventoryLib.ogGetOsVersion (disk, par)
    if 'Windows 1' in osversion:
        loader = None
        for i in f'{efidir}/EFI/Microsoft/Boot/bootmgfw.efi', f'{efidir}/EFI/{bootlabel}/Boot/bootmgfw.efi':
            if os.path.exists (i):
                loader = i
        if not loader:
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTOS, f'{disk} {par} ({osversion}, EFI)')
            return None

        if (os.path.isdir (f'{mntdir}/ogBoot')):
            shutil.rmtree (f'{mntdir}/ogBoot')
        dirloader = os.path.realpath (os.path.dirname (loader) + '/..')
        shutil.copytree (f'{dirloader}/Boot', f'{mntdir}/ogBoot')


#/**
#         ogNvramDeleteEntry
#@brief   Borra entrada de la NVRAM identificada por la etiqueta o el orden
#@param    Num_order_entry | Label_entry Número de orden o la etiqueta de la entrada a borrar.
#@return  (nada)
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTUEFI   UEFI no activa.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado (entrada en NVRAM).
#*/ ##
#ogNvramDeleteEntry ('2')
#ogNvramDeleteEntry ('Windows Boot Manager')
def ogNvramDeleteEntry (entry):
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    numentries = []
    efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
    try:
        foo = int (entry, 16)     ## raises ValueError if entry doesn't look like hexadecimal
        num = f'{foo:04x}'.upper()
        for l in efibootmgr_out.splitlines():
            words = l.split()
            if len(words) < 2: continue
            if num in words[0]:
                numentries.append (words[0][4:8])
    except ValueError:
        for l in efibootmgr_out.splitlines():
            words = l.split (maxsplit=2)
            if len(words) < 3: continue
            if words[1] == entry:
                numentries.append (words[0][4:8])

    if not numentries:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'NVRAM entry "{entry}"')
        return

    if 1 != len(numentries):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_GENERIC, f'more than one entry found')
        return

    subprocess.run (['efibootmgr', '-B', '-b', numentries[0]], capture_output=True, text=True)



#/**
#         ogNvramGetCurrent
#@brief   Muestra la entrada del gestor de arranque (NVRAM) que ha iniciado el equipo.
#@return  Entrada con la que se ha iniciado el equipo
#@exception OG_ERR_NOTUEFI   UEFI no activa.
#*/ ##
def ogNvramGetCurrent():
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    bootentry = '9999'
    ret = None
    efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
    for l in efibootmgr_out.splitlines():
        words = l.split (maxsplit=1)
        if len(words) < 2: continue
        if 'BootCurrent' in words[0]:
            bootentry = words[1]
            continue
        if bootentry in words[0]:
            num = words[0][4:8].strip ('0') or '0'
            ret = f'{num}   {words[1]}'
    return ret


#         ogNvramGetNext
#@brief   Muestra la entrada del gestor de arranque (NVRAM) que se utilizará en el próximo arranque.
#@return  Entrada que se utilizará en el próximo arranque
#@exception OG_ERR_NOTUEFI   UEFI no activa.
#*/ ##
def ogNvramGetNext():
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    ret = None
    efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
    for l in efibootmgr_out.splitlines():
        words = l.split (maxsplit=1)
        if len(words) < 2: continue
        if 'BootNext' in words[0]:
            ret = words[1]
    return ret


#         ogNvramGetOrder
#@brief   Muestra el orden de las entradas del gestor de arranque (NVRAM)
#@return  Array, orden de las entradas
#@exception OG_ERR_NOTUEFI   UEFI no activa.
#*/ ##
def ogNvramGetOrder():
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    ret = []
    efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
    for l in efibootmgr_out.splitlines():
        words = l.split()
        if len(words) < 2: continue
        if 'BootOrder:' == words[0]:
            ret = words[1].split (',')
    return ret


#/**
#         ogNvramGetTimeout
#@brief   Muestra el tiempo de espera del gestor de arranque (NVRAM)
#@return  Timeout de la NVRAM
#@exception OG_ERR_NOTUEFI   UEFI no activa.
#*/ ##
def ogNvramGetTimeout():
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    ret = None
    efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
    for l in efibootmgr_out.splitlines():
        words = l.split (maxsplit=1)
        if len(words) < 2: continue
        if 'Timeout:' == words[0]:
            ret = words[1]
    return ret


#/**
#         ogGrubUefiConf int_ndisk int_part str_dir_grub
#@brief   Genera el fichero grub.cfg de la ESP
#@param   int_ndisk    nº de orden del disco
#@param   int_part     nº de partición
#@param   str_dir_grub prefijo del directorio de grub en la partición de sistema. ej: /boot/grubPARTITION
#@return  (nada, por determinar)
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#@TODO    Confirmar si el fichero "$EFIDIR/EFI/$BOOTLABEL/grub.cfg" es necesario.
#*/ ##


#/**
#         ogNvramInactiveEntry
#@brief   Inactiva entrada de la NVRAM identificada por la etiqueta o el orden
#@param    Num_order_entry | Label_entry Número de orden o la etiqueta de la entrada a borrar.
#@return  (nada)
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#@exception OG_ERR_NOTUEFI   UEFI no activa.
#*/ ##
#ogNvramInactiveEntry ('2')
#ogNvramInactiveEntry ('Windows Boot Manager')
def ogNvramInactiveEntry (entry):
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    numentries = []
    efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
    try:
        foo = int (entry, 16)     ## raises ValueError if entry doesn't look like hexadecimal
        num = f'{foo:04x}'.upper()
        for l in efibootmgr_out.splitlines():
            words = l.split()
            if len(words) < 2: continue
            if num in words[0]:
                numentries.append (words[0][4:8])
    except ValueError:
        for l in efibootmgr_out.splitlines():
            words = l.split (maxsplit=2)
            if len(words) < 3: continue
            if words[1] == entry:
                numentries.append (words[0][4:8])

    if not numentries:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'NVRAM entry "{entry}"')
        return

    if 1 != len(numentries):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_GENERIC, f'more than one entry found')
        return

    subprocess.run (['efibootmgr', '-A', '-b', numentries[0]], capture_output=True, text=True)


#/**
#         ogNvramList
#@brief   Lista las entradas de la NVRAN (sólo equipos UEFI)
#@return  Multiline string: entradas de la NVRAM con el formato: orden etiqueta [* (si está activa) ]
#@exception OG_ERR_NOTUEFI  UEFI no activa.
#*/ ##
def ogNvramList():
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    ret = ''
    efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
    for l in efibootmgr_out.splitlines():
        words = l.split (maxsplit=1)
        if len(words) < 2: continue
        if re.search ('Boot[0-9]', words[0]):
            active = '*' if '*' in words[0] else ''
            num = words[0][4:8].strip ('0') or '0'
            ret += '{:>4s}   {} {}\n'.format (num, words[1], active)
    return ret


#/**
#         ogNvramPxeFirstEntry
#@brief   Sitúa la entrada de la tarjeta de red en el primer lugar en la NVRAM.
#@return  (nada)
#@exception OG_ERR_NOTUEFI  UEFI no activa.
#*/ ##
def ogNvramPxeFirstEntry():
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    num = ''
    efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
    for l in efibootmgr_out.splitlines():
        if re.search ('IP[vV]{0,1}4', l):
            num = l[4:8].strip ('0') or '0'
    numentry = f'{int(num):04x}'.upper()

    o = ogNvramGetOrder()
    if not o:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_GENERIC, f'ogNvramGetOrder returned an empty list')
        return

    # Si la entrada es la primera nos salimos.
    if numentry == o[0]:
        return True

    # Si la entrada ya existe la borramos.
    order = [numentry] + list (filter (lambda x: x if x!=numentry else [], o))
    ogNvramSetOrder (order)

    return True


#/**
#         ogRestoreEfiBootLoader int_ndisk str_repo
#@brief   Copia el cargador de arranque de la partición de sistema a la partición EFI.
#@param   int_ndisk    nº de orden del disco
#@param   int_part     nº de partición
#@return  (nada, por determinar)
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado (partición de sistema o EFI).
#@exception OG_ERR_NOTOS     sin sistema operativo.
#*/ ##
def ogRestoreEfiBootLoader (disk, par):
    mntdir = FileSystemLib.ogMount (disk, par)
    if not mntdir:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, f'{disk} {par}')
        return

    esp = DiskLib.ogGetEsp()
    if not esp:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, 'EFI partition')
        return
    esp_disk, esp_par = esp.split()
    efidir = FileSystemLib.ogMount (esp_disk, esp_par)
    if not efidir:
        FileSystemLib.ogFormat (esp_disk, esp_par, 'FAT32')
        efidir = FileSystemLib.ogMount (esp_disk, esp_par)
        if not efidir:
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, 'ESP')
            return

    osversion = InventoryLib.ogGetOsVersion (disk, par)
    if osversion and 'Windows 1' in osversion:
        bootlabel = f'Part-{int(disk):02d}-{int(par):02d}'
        loader = FileLib.ogGetPath (file=f'{mntdir}/ogBoot/bootmgfw.efi')
        if not loader:
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTOS, f'{disk} {par} ({osversion}, EFI)')
            return
        efi_bl = f'{efidir}/EFI/{bootlabel}'
        if os.path.exists (efi_bl):
            shutil.rmtree (efi_bl)
        os.makedirs (efi_bl, exist_ok=True)
        shutil.copytree (os.path.dirname (loader), f'{efi_bl}/Boot', symlinks=True)
        shutil.copy (loader, f'{efi_bl}/Boot/ogloader.efi')
        if '' != FileLib.ogGetPath (file=f'{efidir}/EFI/Microsoft'):
            os.rename (f'{efidir}/EFI/Microsoft', f'{efidir}/EFI/Microsoft.backup.og')

    return


#/**
#         ogRestoreUuidPartitions
#@brief   Restaura los uuid de las particiones y la tabla de particiones
#@param   int_ndisk      nº de orden del disco
#@param   int_nfilesys   nº de orden del sistema de archivos
#@param   REPO|CACHE     repositorio
#@param   str_imgname    nombre de la imagen
#@return  (nada)
#@exception OG_ERR_FORMAT    Formato incorrecto.
#@exception OG_ERR_NOTFOUND  No encontrado fichero de información de la imagen (con uuid)
#*/ ##


#/**
#         ogNvramSetNext
#@brief   Configura el próximo arranque con la entrada del gestor de arranque (NVRAM) identificada por la etiqueta o el orden.
#@param    Num_order_entry | Label_entry Número de orden o la etiqueta de la entrada a borrar.
#@return  (nada)
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTUEFI   UEFI no activa.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#*/ ##
#ogNvramSetNext ('2')
#ogNvramSetNext ('Windows Boot Manager')
def ogNvramSetNext (entry):
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    numentries = []
    efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
    try:
        foo = int (entry, 16)     ## raises ValueError if entry doesn't look like hexadecimal
        num = f'{foo:04x}'.upper()
        for l in efibootmgr_out.splitlines():
            words = l.split()
            if len(words) < 2: continue
            if num in words[0]:
                numentries.append (words[0][4:8])
    except ValueError:
        for l in efibootmgr_out.splitlines():
            words = l.split (maxsplit=2)
            if len(words) < 3: continue
            if words[1] == entry:
                numentries.append (words[0][4:8])

    if not numentries:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'NVRAM entry "{entry}"')
        return

    if 1 != len (numentries):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_GENERIC, f'more than one entry found')
        return

    SystemLib.ogEcho ([], 'info', f'setting "{numentries[0]}" as next entry in NVRAM')
    subprocess.run (['efibootmgr', '-n', numentries[0]], capture_output=True, text=True)

#/**
#         ogNvramSetOrder
#@brief   Configura el orden de las entradas de la NVRAM
#@param   Orden de las entradas separadas por espacios
#@return  (nada)
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTUEFI   UEFI no activa.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado (entrada NVRAM).
#*/ ##
#ogNvramSetOrder (['1', '3'])
def ogNvramSetOrder (order):
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    try:
        for i in order:
            foo = int (i, 16)
    except ValueError:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, 'ogNvramSetOrder ([array or hex values])')
        return

    # Entradas de la NVRAM actuales
    numentries = []
    efibootmgr_out = subprocess.run (['efibootmgr'], capture_output=True, text=True).stdout
    for l in efibootmgr_out.splitlines():
        words = l.split()
        if len(words) < 2: continue
        if re.search ('Boot[0-9a-fA-F]{4}', words[0]):
            numentries.append ('0' + words[0][4:8])

    new_order = []
    for o in order:
        h = f'{int(o,16):05x}'.upper()
        if h not in numentries:
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'NVRAM entry order "{h}"')
            return
        new_order.append (h)

    subprocess.run (['efibootmgr', '-o', ','.join (new_order)])


#/**
#         ogNvramSetTimeout
#@brief   Configura el tiempo de espera de la NVRAM
#@param   Orden de las entradas separadas por espacios
#@return  (nada)

#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#*/ ##
#ogNvramSetTimeout ('2')
def ogNvramSetTimeout (t):
    if not InventoryLib.ogIsEfiActive():
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTUEFI, '')
        return

    try:
        num = int (t)     ## raises ValueError if t doesn't look like a number
    except ValueError:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, 'ogNvramSetTimeout (timeout)')
        return

    subprocess.run (['efibootmgr', '-t', t])


#/**
#         ogUuidChange int_ndisk str_repo
#@brief   Reemplaza el UUID de un sistema de ficheros.
#@param   int_ndisk    nº de orden del disco
#@param   int_part     nº de partición
#@return  (nada, por determinar)
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#*/ ##
