import os
import re
import subprocess
import shutil
import sys
import platform

import ogGlobals
import SystemLib
import DiskLib
import FileSystemLib

#/**
#         ogCreateCache [int_ndisk] int_partsize
#@brief   Define la caché local, por defecto en partición 4 del disco 1.
#@param   int_ndisk  numero de disco donde crear la cache, si no se indica es el 1 por defecto
#@param   int_npart      número de partición (opcional, 4 por defecto)
#@param   int_partsize   tamaño de la partición (en KB)
#@return  (nada, por determinar)
#@exception OG_ERR_FORMAT   formato incorrecto.
#@note    Requisitos: sfdisk, parted, awk, sed
#@warning El tamaño de caché debe estar entre 50 MB y la mitad del disco.
#@warning La caché no puede solaparse con las particiones de datos.
#*/ ##
def ogCreateCache (ndsk=1, part=4, sizecache=0):
    if not sizecache:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, '')
        return None
    sizecache = int (sizecache)

    DISK = DiskLib.ogDiskToDev (ndsk)
    if not DISK: return None

 # PATCH Para discos nvme la particion debe ser p1, p2, etc...en lugar de 1,2, sino falla sfdisk
    NVME_PREFIX = ''
    if 'nvme' in DISK:
        NVME_PREFIX = 'p'

    END = DiskLib.ogGetLastSector (ndsk)
    if not END:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, 'Failed to get last sector of disk')
        return None
    SIZE = 2 * sizecache
# Inicio partición cache según el disco tenga sectores de 4k o menores
    IOSIZE = DiskLib.ogGetIoSize (ndsk)
    if not IOSIZE:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'Failed to get disk sector size')
        return None
    START = 0
    if 4096 == IOSIZE:
        END -= 8192
        START = END - SIZE + 2048 - (END-SIZE)%2048
    else:
        START = END - SIZE + 1

    ENDPREVPART = None
    i = 1
    while True:
        prev_part = part - i
        if prev_part <= 0: break
        ENDPREVPART = DiskLib.ogGetLastSector (ndsk, prev_part)
        if ENDPREVPART: break
        i += 1
    if not ENDPREVPART:
        ENDPREVPART=0
        #SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, ndsk)
        #return None
# Error si tamaño no está entre límites permitidos o si se solapa con la partición anterior.
    MINSIZE = 25000
    MAXSIZE = END
    if SIZE < MINSIZE or SIZE > MAXSIZE or START < ENDPREVPART:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'size ({SIZE}) < minsize ({MINSIZE}) or size ({SIZE}) > maxsize ({MAXSIZE}) or start ({START}) < endprevpart ({ENDPREVPART})')
        return None

# Desmontar todos los sistemas de archivos del disco.
    FileSystemLib.ogUnmountAll (ndsk)
# Definir particiones y notificar al kernel.
# En el caso de ser disco GPT, de momento se borra la particion y se vuelve a crear,
# por lo que se pierden los datos.
    pttype = DiskLib.ogGetPartitionTableType (ndsk)
    if not pttype:
        pttype = 'MSDOS'
        DiskLib.ogCreatePartitionTable (ndsk, pttype)

    get_ptt = DiskLib.ogGetPartitionTableType (ndsk)
    if 'GPT' == get_ptt:
        # Si la tabla de particiones no es valida, volver a generarla.
        if subprocess.run (['sgdisk', '-p', DISK], capture_output=True, text=True).returncode:     ## if True, then sgdisk failed
            subprocess.run (['gdisk', DISK], input='2\nw\nY\n', capture_output=True, text=True)
        # Si existe la cache se borra previamente
        if ogFindCache(): ogDeleteCache()

        ID = DiskLib.ogTypeToId ('LINUX', 'GPT_GUID')
        sfdisk_line = f'{DISK}{NVME_PREFIX}{part} : start= {START}, size= {SIZE}, type={ID}, name=CACHE'
    elif 'MSDOS' == get_ptt:
        # Si la tabla de particiones no es valida, volver a generarla.
        if subprocess.run (['parted', '-s', DISK, 'print'], capture_output=True).returncode:     ## if True, then parted failed
            subprocess.run (['fdisk', DISK], input='w\n', text=True, capture_output=True)

        ID = DiskLib.ogTypeToId ('CACHE', 'MSDOS')
        sfdisk_line = f'{DISK}{NVME_PREFIX}{part} : start= {START}, size= {SIZE}, Id={ID}'
    else:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, 'No hay tabla de particiones en el disco')
        return False

    # Salvamos la configuración de las particiones e incluimos la cache.
    tmp = subprocess.run (['sfdisk', '--dump', DISK], capture_output=True, text=True).stdout.splitlines()
    tmp = [ x for x in tmp if f'{DISK}{part}' not in x ]      ## grep -v la_de_cache
    tmp.append (sfdisk_line)
    # Ordenamos las líneas de los dispositivos
    UNIT = [ x for x in tmp if 'unit' in x ][0]
    tmp = sorted ([ x for x in tmp if re.match ('^/dev', x) ])
    tmp = [UNIT, ''] + tmp
    # Guardamos nueva configuración en el disco.
    i = '\n'.join(tmp)
    subprocess.run (['sfdisk', '--no-reread', DISK], input=i, text=True)

# Actualiza la tabla de particiones en el kernel.
    DiskLib.ogUpdatePartitionTable()
    return True



#/**
#         ogDeleteCache
#@brief   Elimina la partición de caché local.
#@return  (nada, por determinar)
#@exception OG_ERR_FORMAT   formato incorrecto.
#@note    Requisitos: fdisk, sgdisk, partprobe
#*/ ##
def ogDeleteCache():
    cachepart = ogFindCache()
    if not cachepart:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, ogGlobals.lang.MSG_NOCACHE)
        return None

    ndisk, npart = cachepart.split()
    disk = DiskLib.ogDiskToDev (ndisk)

# Desmontar todos los sistemas de archivos del disco.
    FileSystemLib.ogUnmountAll (ndisk)

    ptt = DiskLib.ogGetPartitionTableType (ndisk)
    if 'GPT' == ptt:
        # Si la tabla de particiones no es valida, volver a generarla.
        if subprocess.run (['sgdisk', '-p', disk], capture_output=True).returncode:     ## if True, then sgdisk failed
            subprocess.run (['gdisk', disk], input='2\nw\nY\n', text=True, capture_output=True)
        subprocess.run (['sgdisk', disk, f'-d{npart}'])
    elif 'MSDOS' == ptt:
        # Si la tabla de particiones no es valida, volver a generarla.
        if subprocess.run (['parted', '-s', disk, 'print'], capture_output=True).returncode:     ## if True, then parted failed
            subprocess.run (['fdisk', disk], input='w', text=True, capture_output=True)
        # Eliminar la partición de caché.
        subprocess.run (['fdisk', disk], input=f'd\n{npart}\nw', text=True, capture_output=True)
# Borrar etiqueta de la caché.
    if os.path.exists ('/dev/disk/by-label/CACHE'):
        os.unlink ('/dev/disk/by-label/CACHE')
#Actualiza la tabla de particiones en el kernel.
    DiskLib.ogUpdatePartitionTable()

#/**
#         ogFindCache
#@brief   Detecta la partición caché local.
#@param   No requiere parametros
#@return  int_ndisk int_npart - devuelve el par nº de disco-nº de partición .
#@warning Si no hay cache no devuelve nada
#*/ ##
def ogFindCache():
    # Obtener el dispositivo del sistema de archivos etiquetado como "CACHE".
    PART = subprocess.run (['blkid', '-L', 'CACHE'], capture_output=True, text=True).stdout.strip()

    # En discos nvme con particiones GPT la partición se detecta usando el tag PARTLABEL
    if not PART:
        out = subprocess.run (['blkid', '-t', 'PARTLABEL=CACHE'], capture_output=True, text=True).stdout.strip()
        PART = out.split (':')[0]

    # Si no se detecta, obtener particiones marcadas de tipo caché en discos MSDOS.
    if not PART:
        out = subprocess.run (['sfdisk', '-l'], capture_output=True, text=True).stdout.splitlines()
        for l in out:
            elems = l.split (maxsplit=5)
            if 6 > len (elems): continue
            if 'ca' in elems[5] or 'a7' in elems[5]:
                PART=elems[0]
                break

    # Por último revisar todos los discos GPT y obtener las particiones etiquetadas como caché.
    if not PART:
        PART = ''
        for d in DiskLib.ogDiskToDev():
            out = subprocess.run (['sgdisk', '-p', d], capture_output=True, text=True).stdout.splitlines()
            for l in out:
                elems = l.split (maxsplit=6)
                if 7 > len (elems): continue
                if 'CACHE' in elems[6]:
                    p = 'p' if 'nvme' in d else ''
                    PART += f' {d}{p}{elems[0]}'

    if not PART: return
    return DiskLib.ogDevToDisk (PART.split()[0])    # usar la 1ª partición encontrada.


#/**
#         ogFormatCache
#@brief   Formatea el sistema de ficheros para la caché local.
#@return  (por determinar)
#@warning Prueba con formato Reiser.
#@attention
#@note    El sistema de archivos de la caché se queda montado.
#*/ ##
def ogFormatCache():
    cachepart = ogFindCache()
    if cachepart is None:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, ogGlobals.lang.MSG_NOCACHE)
        return

    cachepart = cachepart.split()
    disk = DiskLib.ogDiskToDev (cachepart[0], cachepart[1])
    if not disk: return

    ogUnmountCache()
    options = "extent,large_file"
    if re.match("^5", platform.release()):
        options += ",uninit_bg,^metadata_csum,^64bit"

    try:
        subprocess.run(["mkfs.ext4", "-q", "-F", disk, "-L", "CACHE", "-O", options], check=True)
    except subprocess.CalledProcessError as e:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, "CACHE")
        return

    # Crear estructura básica.
    mntdir = ogMountCache()
    j = '/'.join ([mntdir, ogGlobals.OGIMG])     ## os.path.join doesn't work: "If a segment […] is an absolute path, all previous segments are ignored"
    #print (f'cucu mntdir ({mntdir}) OGIMG ({ogGlobals.OGIMG}) j ({j})')
    os.makedirs (j, exist_ok=True)

    # Incluir kernel e Initrd del ogLive
    subprocess.run ([f'{ogGlobals.OGSCRIPTS}/updateBootCache'])


#/**
#         ogGetCacheSize
#@brief   Devuelve el tamaño definido para la partición de caché.
#@return  int_partsize   tamaño de la partición (en KB)
#@exception OG_ERR_PARTITION  No existe partición de caché.
#*/ ##
def ogGetCacheSize():
    cachepart = ogFindCache()
    if cachepart is None:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, ogGlobals.lang.MSG_NOCACHE)
        return None

    disk, par = cachepart.split()
    return DiskLib.ogGetPartitionSize (disk, par)

#/**
#         ogGetCacheSpace
#@brief   Devuelve el espacio de disco disponible para la partición de caché.
#@return  int_size   tamaño disponible (en KB)
#@note    El espacio disponible es el que hay entre el límite superior de la partición 3 del disco 1 y el final de dicho disco, y no puede ser superior a la mitad de dicho disco.
#*/ ##
def ogGetCacheSpace():
    cachepart = ogFindCache()
    if not cachepart:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, ogGlobals.lang.MSG_NOCACHE)
        return None
    cachepart = cachepart.split()

    disk = DiskLib.ogDiskToDev (cachepart[0])
    if not disk:
        return None

    sectors = 0
    disk_bn = os.path.basename (disk)
    with open ('/proc/partitions', 'r') as fd:
        proc_parts = fd.read()
    for l in proc_parts.splitlines():
        items = l.split()
        if len(items) < 4: continue
        if items[3] == disk_bn:
            sectors = 2 * int (items[2])
    if not sectors: return None

    sfdisk_out = subprocess.run (['sfdisk', '-g', disk], capture_output=True, text=True).stdout
    cyls = int (sfdisk_out.split()[1])
    sectors = sectors/cyls * cyls - 1

    ## the original code has a hard dependency on the existence of a third partition
    ## if the disk has sda1, sda2 and sda4, the code fails.
    ## this is an improved version
    endpart3 = 0
    for trypart in [3, 2, 1]:
        sfdisk_out = subprocess.run (['sfdisk', '-uS', '-l', disk], capture_output=True, text=True).stdout
        for l in sfdisk_out.splitlines():
            items = l.split()
            if len(items) < 6: continue
            if f'{disk}{trypart}' == items[0]:
                endpart3 = int (items[2])
                break
        if endpart3: break
    if not endpart3: return None

# Mostrar espacio libre en KB (1 KB = 2 sectores)
    if endpart3 > sectors // 2:
        return (sectors - endpart3) // 2
    else:
        return sectors // 4


#/**
#         ogMountCache
#@brief   Monta la partición Cache y exporta la variable $OGCAC
#@param   sin parametros
#@return  path_mountpoint - Punto de montaje del sistema de archivos de cache.
#@warning Salidas de errores no determinada
#*/ ##
def ogMountCache():
    c = ogFindCache()
    if not c:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, ogGlobals.lang.MSG_NOCACHE)
        return None

    c = c.split()
    m = FileSystemLib.ogMountFs (c[0], c[1])
    if not m:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, ogGlobals.lang.MSG_NOCACHE)
        return None
    return m


#/**
#         ogUnmountCache
#@brief   Desmonta la particion Cache y elimina la variable $OGCAC
#@param   sin parametros
#@return  nada
#@warning Salidas de errores no determinada
#*/ ##
def ogUnmountCache():
    cachepart = ogFindCache()
    if cachepart is None:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, ogGlobals.lang.MSG_NOCACHE)
        return

    cachepart = cachepart.split()
    if not FileSystemLib.ogIsMounted (cachepart[0], cachepart[1]): return True
    FileSystemLib.ogUnmountFs (cachepart[0], cachepart[1])

    # Eliminar el enlace simbólico de /mnt/ParticiónCache.
    dev = DiskLib.ogDiskToDev (cachepart[0], cachepart[1])
    dev = dev.replace ('dev', 'mnt')
    if os.path.exists (dev): os.remove (dev)


#/**
#         initCache
#@brief   Simplemente llama al script initCache
#@brief   Es necesario tener una función para poder pasársela a ogExecAndLog
#@param   los mismos parametros que initCache
#@return  lo mismo que devuelve initCache
#*/ ##
def initCache (*args):
    p = subprocess.run ([f'{ogGlobals.OGSCRIPTS}/initCache'] + list(args))
    return not p.returncode       ## negate shell return code


#/**
#         ogCheckNewCacheSize
#@brief   Comprueba si un cache de X tamaño cabe en el hueco que dejan las demás particiones
#@param   Tamaño de la nueva hipotética cache
#@return  Boolean, True si la nueva cache cabría, False si no
#*/ ##
def ogCheckNewCacheSize (disk, kB):
    DISK = DiskLib.ogDiskToDev (disk)
    if not DISK: return None

    IOSIZE = DiskLib.ogGetIoSize (disk)
    if not IOSIZE:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'Failed to get disk sector size')
        return None
    last_sector = DiskLib.ogGetLastSector (disk)

    end_of_last_partition = 0
    cachepart = ogFindCache()
    if cachepart:
        cache_disk, cache_part = cachepart.split()
        cache_dev = DiskLib.ogDiskToDev (cache_disk, cache_part)
    else:
        cache_dev = None
    fdisk_l_out = subprocess.run (['fdisk', '-l', DISK], capture_output=True, text=True).stdout
    for l in fdisk_l_out.splitlines():
        if not re.match ('^/dev', l):                     ## no empieza por /dev, nos la saltamos
            continue
        if cache_dev and re.match (f'^{cache_dev}', l):   ## es la de cache, nos la saltamos
            continue
        last_sector_of_partition = int (l.split()[2])
        if last_sector_of_partition > end_of_last_partition:
            end_of_last_partition = last_sector_of_partition

    if 512 == IOSIZE:
        cache_sectors = kB*2
    else:   ## 4096
        cache_sectors = (kB+3)//4    ## sumamos 3 para que la división entera "redondee al alza"
    if end_of_last_partition + cache_sectors > last_sector:
         SystemLib.ogEcho (['session', 'log'], None, f'end_of_last_partition ({end_of_last_partition}) + cache_sectors ({cache_sectors}) > last_sector ({last_sector}), check failed')
         return False
    return True
