#!/usr/bin/env bash
set -euo pipefail
source "$stpRoot/lib/log.sh"
source "$stpRoot/lib/utils.sh"
source "$stpRoot/lib/yaml.sh"

readonly registryConfig="$stpRoot/config/registry.yaml"

fieldOf() {
    local entryId="$1" field="$2" default="${3:-}"
    local value
    value="$(yq ".registry[] | select(.id == \"$entryId\") | .${field}" "$registryConfig" 2>/dev/null)"
    if util::isYamlNull "$value"; then
        echo "$default"
    else
        echo "$value"
    fi
}

arrayFieldOf() {
    local entryId="$1" field="$2"
    yq -r ".registry[] | select(.id == \"$entryId\") | .${field}[]" "$registryConfig" 2>/dev/null || true
}

# ---- PPA ----

ppaOwnerFrom() {
    local ppaAddress="$1"
    local owner="${ppaAddress#ppa:}"
    echo "${owner%%/*}"
}

ppaIsAlreadyAdded() {
    local ppaAddress="$1"
    find /etc/apt/sources.list.d/ -name "*$(ppaOwnerFrom "$ppaAddress")*" 2>/dev/null | grep -q .
}

applyPpa() {
    local entryId="$1"
    local ppaAddress
    ppaAddress="$(fieldOf "$entryId" "address")"

    if ppaIsAlreadyAdded "$ppaAddress"; then
        log::info "PPA ya existe: $ppaAddress"
        return
    fi

    util::cmdExists add-apt-repository \
        || sudo apt-get install -y software-properties-common

    sudo add-apt-repository -y "$ppaAddress"
    sudo apt-get update -qq
    log::ok "PPA agregado: $ppaAddress"
}

# ---- APT ----

installedVersionOf() {
    local packageName="$1"
    dpkg-query -W -f='${Version}' "$packageName" 2>/dev/null
}

aptVersionIsInRange() {
    local installedVersion="$1" minVersion="$2" maxVersion="$3"
    if [[ -n "$minVersion" ]] && dpkg --compare-versions "$installedVersion" lt "$minVersion"; then
        return 1
    fi
    if [[ -n "$maxVersion" ]] && dpkg --compare-versions "$installedVersion" gt "$maxVersion"; then
        return 1
    fi
    return 0
}

applyAptPackage() {
    local entryId="$1"
    local packageName minVersion maxVersion
    packageName="$(fieldOf "$entryId" "package" "$entryId")"
    minVersion="$(fieldOf "$entryId" "minVersion")"
    maxVersion="$(fieldOf "$entryId" "maxVersion")"

    if util::isAptInstalled "$packageName"; then
        local installedVersion
        installedVersion="$(installedVersionOf "$packageName")"
        if aptVersionIsInRange "$installedVersion" "$minVersion" "$maxVersion"; then
            log::info "Ya instalado (apt): $packageName ($installedVersion)"
            return
        fi
        log::warn "$packageName: versión $installedVersion instalada, fuera del rango requerido"
    fi

    util::aptUpdateOnce
    if [[ -n "$maxVersion" ]]; then
        sudo apt-get install -y --allow-downgrades "${packageName}=${maxVersion}"
        log::ok "Instalado (apt): ${packageName}=${maxVersion}"
    else
        sudo apt-get install -y "$packageName"
        log::ok "Instalado (apt): $packageName"
    fi
}

# ---- SNAP ----

applySnapPackage() {
    local entryId="$1"
    local packageName classicFlag installFlags
    packageName="$(fieldOf "$entryId" "package" "$entryId")"
    classicFlag="$(fieldOf "$entryId" "classic" "false")"
    installFlags=()
    [[ "$classicFlag" == "true" ]] && installFlags+=(--classic)

    if util::isSnapInstalled "$packageName"; then
        log::info "Ya instalado (snap): $packageName"
        return
    fi

    if ! util::cmdExists snap; then
        log::warn "snap no disponible, salteando: $entryId"
        return
    fi

    sudo snap install "$packageName" "${installFlags[@]}"
    log::ok "Instalado (snap): $packageName"
}

# ---- FLATPAK ----

applyFlatpakApp() {
    local entryId="$1"
    local appId remote
    appId="$(fieldOf "$entryId" "appId" "$entryId")"
    remote="$(fieldOf "$entryId" "remote" "flathub")"

    if util::isFlatpakInstalled "$appId"; then
        log::info "Ya instalado (flatpak): $appId"
        return
    fi

    if ! util::cmdExists flatpak; then
        log::warn "flatpak no disponible, salteando: $entryId"
        return
    fi

    flatpak install -y "$remote" "$appId"
    log::ok "Instalado (flatpak): $appId"
}

# ---- DOTFILE ----

dotfileSymlinkAlreadyExists() {
    local destination="$1"
    [[ -L "$destination" ]]
}

backupExistingFile() {
    local destination="$1"
    [[ -e "$destination" ]] || return 0
    mv "$destination" "${destination}.stpbackup"
    log::info "Copia de seguridad: ~/${destination#${HOME}/}"
}

deployFileFromElement() {
    local sourceFile="$1" elementSourceDirectory="$2"
    local relativePath destination
    relativePath="${sourceFile#${elementSourceDirectory}/}"
    destination="$HOME/$relativePath"

    if dotfileSymlinkAlreadyExists "$destination"; then
        log::info "Enlace ya existe: ~/$relativePath"
        return
    fi

    backupExistingFile "$destination"
    mkdir -p "$(dirname "$destination")"
    ln -sfn "$sourceFile" "$destination"
    log::ok "Enlazado: ~/$relativePath"
}

applyDotfile() {
    local entryId="$1"
    local elementSourceDirectory="$stpRoot/dotfiles/$entryId"

    if [[ ! -d "$elementSourceDirectory" ]]; then
        log::warn "Carpeta no encontrada: dotfiles/$entryId/"
        return
    fi

    while IFS= read -r sourceFile; do
        deployFileFromElement "$sourceFile" "$elementSourceDirectory"
    done < <(find "$elementSourceDirectory" -not -type d | sort)
}

# ---- SERVICE ----

applyService() {
    local entryId="$1"
    local serviceName scope state
    serviceName="$(fieldOf "$entryId" "name" "$entryId")"
    scope="$(fieldOf "$entryId" "scope" "system")"
    state="$(fieldOf "$entryId" "state" "enable")"

    local scopeFlags=()
    [[ "$scope" == "user" ]] && scopeFlags+=(--user)

    case "$state" in
        enable)
            systemctl "${scopeFlags[@]}" enable --now "$serviceName" 2>/dev/null \
                || log::warn "No se pudo habilitar: $serviceName"
            ;;
        disable)
            systemctl "${scopeFlags[@]}" disable --now "$serviceName" 2>/dev/null || true
            ;;
        mask)
            systemctl "${scopeFlags[@]}" mask "$serviceName" 2>/dev/null || true
            ;;
    esac
    log::ok "Servicio [$scope] $state: $serviceName"
}

# ---- PIPEWIRE ----

allPipewirePackagesAreInstalled() {
    local entryId="$1"
    while IFS= read -r packageName; do
        util::isYamlNull "$packageName" && continue
        util::isAptInstalled "$packageName" || return 1
    done < <(arrayFieldOf "$entryId" "packages")
}

installMissingPipewirePackages() {
    local entryId="$1"
    local missingPackages=()
    while IFS= read -r packageName; do
        util::isYamlNull "$packageName" && continue
        util::isAptInstalled "$packageName" || missingPackages+=("$packageName")
    done < <(arrayFieldOf "$entryId" "packages")

    if [[ ${#missingPackages[@]} -gt 0 ]]; then
        util::aptUpdateOnce
        sudo apt-get install -y "${missingPackages[@]}"
    fi
}

disablePulseaudio() {
    systemctl --user --now disable pulseaudio.service pulseaudio.socket 2>/dev/null || true
    systemctl --user mask pulseaudio 2>/dev/null || true
}

enablePipewireUserServices() {
    local entryId="$1"
    while IFS= read -r serviceName; do
        util::isYamlNull "$serviceName" && continue
        systemctl --user --now enable "$serviceName" 2>/dev/null \
            || log::warn "No se pudo habilitar servicio de usuario: $serviceName"
    done < <(arrayFieldOf "$entryId" "userServices")
}

applyPipewire() {
    local entryId="$1"

    allPipewirePackagesAreInstalled "$entryId" \
        || installMissingPipewirePackages "$entryId"

    local shouldReplace
    shouldReplace="$(fieldOf "$entryId" "replacePulseaudio" "false")"
    [[ "$shouldReplace" == "true" ]] && disablePulseaudio

    enablePipewireUserServices "$entryId"
    log::ok "PipeWire configurado"
}

# ---- VIDEO ----

detectGpuVendor() {
    if lspci 2>/dev/null | grep -qi nvidia;          then echo "nvidia"
    elif lspci 2>/dev/null | grep -qi "amd\|radeon"; then echo "amd"
    elif lspci 2>/dev/null | grep -qi intel;         then echo "intel"
    else echo "unknown"
    fi
}

applyVideoDrivers() {
    local entryId="$1"
    local gpuVendor
    gpuVendor="$(detectGpuVendor)"
    log::info "GPU detectada: $gpuVendor"

    if [[ "$gpuVendor" == "unknown" ]]; then
        log::warn "No se pudo identificar la GPU, salteando drivers"
        return
    fi

    local missingPackages=()
    while IFS= read -r packageName; do
        util::isYamlNull "$packageName" && continue
        util::isAptInstalled "$packageName" || missingPackages+=("$packageName")
    done < <(arrayFieldOf "$entryId" "drivers.${gpuVendor}.packages")

    if [[ ${#missingPackages[@]} -eq 0 ]]; then
        log::info "Drivers $gpuVendor ya instalados"
        return
    fi

    util::aptUpdateOnce
    sudo apt-get install -y "${missingPackages[@]}"
    log::ok "Drivers $gpuVendor instalados"
}

# ---- DOCKER ----

dockerIsInstalled() {
    util::cmdExists docker
}

findDockerComposeFile() {
    local destinationDirectory="$1"
    local composeFile
    for composeFile in "compose.yaml" "compose.yml" "docker-compose.yaml" "docker-compose.yml"; do
        [[ -f "$destinationDirectory/$composeFile" ]] && echo "$destinationDirectory/$composeFile" && return 0
    done
    return 1
}

deployDockerConfigFile() {
    local sourceFile="$1" sourceDirectory="$2" destinationDirectory="$3"
    local relativePath destination
    relativePath="${sourceFile#${sourceDirectory}/}"
    destination="$destinationDirectory/$relativePath"

    if [[ -L "$destination" ]]; then
        log::info "Enlace ya existe: $relativePath"
        return
    fi

    [[ -e "$destination" ]] && mv "$destination" "${destination}.stpbackup"
    mkdir -p "$(dirname "$destination")"
    ln -sfn "$sourceFile" "$destination"
    log::ok "Enlazado: $relativePath"
}

applyDocker() {
    local entryId="$1"
    local rawDestination destination autostart
    rawDestination="$(fieldOf "$entryId" "destination" "~/docker/$entryId")"
    destination="${rawDestination/#\~/$HOME}"
    autostart="$(fieldOf "$entryId" "autostart" "false")"

    local sourceDirectory="$stpRoot/docker/$entryId"

    if [[ ! -d "$sourceDirectory" ]]; then
        log::warn "Carpeta no encontrada: docker/$entryId/"
        return
    fi

    if ! dockerIsInstalled; then
        log::warn "Docker no instalado, salteando: $entryId"
        return
    fi

    mkdir -p "$destination"

    while IFS= read -r sourceFile; do
        deployDockerConfigFile "$sourceFile" "$sourceDirectory" "$destination"
    done < <(find "$sourceDirectory" -not -type d | sort)

    if [[ "$autostart" == "true" ]]; then
        local composeFile
        if composeFile="$(findDockerComposeFile "$destination")"; then
            docker compose -f "$composeFile" up -d \
                || log::warn "No se pudo iniciar el servicio: $entryId"
            log::ok "Servicio Docker iniciado: $entryId"
        else
            log::warn "No se encontró archivo compose en: $destination"
        fi
    fi

    log::ok "Docker configurado: $entryId"
}

# ---- Dispatcher ----

dispatchByType() {
    local entryId="$1" entryType="$2"
    case "$entryType" in
        ppa)      applyPpa "$entryId" ;;
        apt)      applyAptPackage "$entryId" ;;
        snap)     applySnapPackage "$entryId" ;;
        flatpak)  applyFlatpakApp "$entryId" ;;
        dotfile)  applyDotfile "$entryId" ;;
        service)  applyService "$entryId" ;;
        pipewire) applyPipewire "$entryId" ;;
        video)    applyVideoDrivers "$entryId" ;;
        docker)   applyDocker "$entryId" ;;
        *)        log::warn "Tipo no reconocido '$entryType' en entrada: $entryId" ;;
    esac
}

processRegistry() {
    local totalProcessed=0
    while IFS=$'\t' read -r entryId entryType; do
        util::isYamlNull "$entryId" && continue
        util::isYamlNull "$entryType" && continue
        log::info "[$entryType] $entryId"
        dispatchByType "$entryId" "$entryType"
        ((++totalProcessed))
    done < <(yq -r '.registry[] | [.id, .type] | @tsv' "$registryConfig")
    log::ok "$totalProcessed entrada(s) del registro procesadas"
}

if ! yaml::has "$registryConfig" ".registry[0]"; then
    log::info "Registro vacío, salteando"
    exit 0
fi

processRegistry
