#!/bin/bash

set -e

export PATH=/usr/local/bin:/usr/bin:/bin

# Color variables
CYAN='\033[33;36m';
RED='\033[0;31m'
YELLOW='\033[0;93m'
GREEN='\033[0;32m'
NC='\033[0m'

# Print provided formatted error and exit with code (default 1)
throw()
{
    echo -e "${RED}${1}${NC}"
    exit "${2:-1}"
}

# Navigate to repository
scriptLocation=$(dirname -- "$(readlink -fn -- "$0"; echo x)")
cd "${scriptLocation%x}"

# Import environment variables
if [[ ! -f .env ]]; then
    throw "Error: .env does not exist" 2
fi
# shellcheck disable=SC1091
source .env

# Define docker command
docker="${DOCKER_COMMAND:-docker}"
if [[ ${docker} != "docker" && ${docker} != "podman" ]]; then
    throw "Error: Invalid DOCKER_COMMAND"
fi

set +e

# Check if docker is installed
${docker} --version > /dev/null 2>&1
dockerInstalled=$?

# Define docker compose command
dockerCompose="${docker} compose"

# Check if docker compose is installed
${dockerCompose} version > /dev/null 2>&1
composeInstalled=$?
if [[ ${composeInstalled} -gt 0 ]]; then
    dockerCompose="${docker}-compose"
    ${dockerCompose} --version > /dev/null 2>&1
    composeInstalled=$?
fi

set -e

# Exit if docker and/or docker compose is not installed
if [[ ${dockerInstalled} -gt 0 || ${composeInstalled} -gt 0 ]]; then
    if [[ ${dockerInstalled} -eq 0 && ${composeInstalled} -gt 0 ]]; then
        throw "Error: ${dockerCompose} not installed."
    fi

    if [[ ${dockerInstalled} -gt 0 && ${composeInstalled} -eq 0 ]]; then
        throw "Error: ${docker} not installed."
    fi

    throw "Error: ${docker} and ${dockerCompose} not installed."
fi

# Add docker compose file arguments to command
composeFiles="-f compose.yml"
if [[ ${APP_ENV} == "development" ]]; then
    composeFiles="${composeFiles} -f compose.dev.yml"
fi
if [[ ${CONTAINER_MODE} == "local" ]]; then
    composeFiles="${composeFiles} -f compose.local.yml"
fi
if [[ -f compose.override.yml ]]; then
    composeFiles="${composeFiles} -f compose.override.yml"
elif [[ -f docker-compose.override.yml ]]; then
    composeFiles="${composeFiles} -f docker-compose.override.yml"
fi
dockerCompose="${dockerCompose} ${composeFiles}"

# Parse services from arguments string
handleServices()
{
    # shellcheck disable=SC2206
    validServices=($1)
    servicesArray=()
    invalidServices=false

    for x in "${@:2}"; do
        if [[ ${validServices[*]} =~ (^|[[:space:]])"$x"($|[[:space:]]) ]]; then
            if ! [[ ${servicesArray[*]} =~ (^|[[:space:]])"$x"($|[[:space:]]) ]]; then
                servicesArray+=("${x}")
            fi
        else
            if [[ $invalidServices == false ]]; then
                invalidServices="${x}"
            else
                invalidServices="${invalidServices} ${x}"
            fi
        fi
    done

    if [[ $invalidServices == false && ${#servicesArray[@]} -gt 0 ]]; then
        echo "1|${servicesArray[*]}"
    elif [[ $invalidServices == false ]]; then
        echo "1|all"
    else
        echo "0|Invalid Service(s): ${invalidServices}"
    fi
}

# Execute a docker command
runDockerCommand()
{
    validCommands=(start stop restart pull build ps logs)
    if ! [[ ${validCommands[*]} =~ (^|[[:space:]])"$2"($|[[:space:]]) ]]; then
        throw "Error: Invalid runDockerCommand input"
    fi

    servicesString=$(handleServices "backend frontend mongo redis" "${@:3}")
    if [[ ${servicesString:0:1} != 1 ]]; then
        throw "${servicesString:2}\n${YELLOW}Usage: ${1} [backend, frontend, mongo, redis]"
    fi

    if [[ ${servicesString:2:4} == "all" ]]; then
        servicesString=""
        pullServices="mongo redis"
        buildServices="backend frontend"
    else
        servicesString=${servicesString:2}
        pullArray=()
        buildArray=()

        if [[ "${servicesString}" == *mongo* ]]; then
            pullArray+=("mongo")
        fi

        if [[ "${servicesString}" == *redis* ]]; then
            pullArray+=("redis")
        fi

        if [[ "${servicesString}" == *backend* ]]; then
            buildArray+=("backend")
        fi

        if [[ "${servicesString}" == *frontend* ]]; then
            buildArray+=("frontend")
        fi

        pullServices="${pullArray[*]}"
        buildServices="${buildArray[*]}"
    fi

    if [[ ${2} == "stop" || ${2} == "restart" ]]; then
        # shellcheck disable=SC2086
        ${dockerCompose} stop ${servicesString}
    fi

    if [[ ${2} == "start" || ${2} == "restart" ]]; then
        # shellcheck disable=SC2086
        ${dockerCompose} up -d ${servicesString}
    fi

    if [[ ${2} == "pull" && ${pullServices} != "" ]]; then
        # shellcheck disable=SC2086
        ${dockerCompose} pull ${pullServices}
    fi

    if [[ ${2} == "build" && ${buildServices} != "" ]]; then
        # shellcheck disable=SC2086
        ${dockerCompose} build ${buildServices}
    fi

    if [[ ${2} == "ps" || ${2} == "logs" ]]; then
        # shellcheck disable=SC2086
        ${dockerCompose} "${2}" ${servicesString}
    fi
}

# Get docker container id
getContainerId()
{
    if [[ $docker == "docker" ]]; then
        containerId=$(${dockerCompose} ps -q "${1}")
    else
        containerId=$(${dockerCompose} ps \
            | sed '0,/CONTAINER/d' \
            | awk "/${1}/ {print \$1;exit}")
    fi
    echo "${containerId}"
}

# Reset services
handleReset()
{
    servicesString=$(handleServices "backend frontend mongo redis" "${@:2}")
    if [[ ${servicesString:0:1} != 1 ]]; then
        throw "${servicesString:2}\n${YELLOW}Usage: ${1} [backend, frontend, mongo, redis]"
    fi

    confirmMessage="${GREEN}Are you sure you want to reset all data"
    if [[ ${servicesString:2:4} != "all" ]]; then
        confirmMessage="${confirmMessage} for $(echo "${servicesString:2}" | tr ' ' ',')"
    fi
    echo -e "${confirmMessage}? ${YELLOW}[y,n]: ${NC}"

    read -r confirm
    if [[ "${confirm}" != y* ]]; then
        throw "Cancelled reset"
    fi

    if [[ ${servicesString:2:4} == "all" ]]; then
        runDockerCommand "$(basename "$0") $1" stop
        ${dockerCompose} rm -v --force
    else
        # shellcheck disable=SC2086
        runDockerCommand "$(basename "$0") $1" stop ${servicesString:2}
        # shellcheck disable=SC2086
        ${dockerCompose} rm -v --force ${servicesString:2}
    fi
}

# Attach to service in docker container
attachContainer()
{
    containerId=$(getContainerId "${2}")
    if [[ -z $containerId ]]; then
        throw "Error: ${2} offline, please start to attach."
    fi

    case $2 in
        backend)
            echo -e "${YELLOW}Detach with CTRL+P+Q${NC}"
            ${docker} attach "$containerId"
            ;;

        mongo)
            MONGO_VERSION_INT=${MONGO_VERSION:0:1}
            echo -e "${YELLOW}Detach with CTRL+D${NC}"
            if [[ $MONGO_VERSION_INT -ge 5 ]]; then
                ${dockerCompose} exec mongo mongosh musare -u "${MONGO_USER_USERNAME}" -p "${MONGO_USER_PASSWORD}" --eval "disableTelemetry()" --shell
            else
                ${dockerCompose} exec mongo mongo musare -u "${MONGO_USER_USERNAME}" -p "${MONGO_USER_PASSWORD}"
            fi
            ;;

        redis)
            echo -e "${YELLOW}Detach with CTRL+C${NC}"
            ${dockerCompose} exec redis redis-cli -a "${REDIS_PASSWORD}"
            ;;

        *)
            throw "Invalid service ${2}\n${YELLOW}Usage: ${1} [backend, mongo, redis]"
            ;;
    esac
}

# Lint codebase, docs, scripts, etc
handleLinting()
{
    set +e
    # shellcheck disable=SC2001
    services=$(sed "s/\(\ \)\{0,1\}\(-\)\{0,2\}fix//g;t;q1" <<< "${@:2}")
    fixFound=$?
    if [[ $fixFound -eq 0 ]]; then
        fix="--fix"
        echo -e "${GREEN}Auto-fix enabled${NC}"
    fi
    # shellcheck disable=SC2001
    services=$(sed "s/\(\ \)\{0,1\}\(-\)\{0,2\}no-cache//g;t;q1" <<< "${services}")
    noCacheFound=$?
    cache="--cache"
    if [[ $noCacheFound -eq 0 ]]; then
        cache=""
        echo -e "${YELLOW}ESlint cache disabled${NC}"
    fi
    set -e

    # shellcheck disable=SC2068
    servicesString=$(handleServices "backend frontend docs shell" ${services[@]})
    if [[ ${servicesString:0:1} != 1 ]]; then
        throw "${servicesString:2}\n${YELLOW}Usage: ${1} [backend, frontend, docs, shell] [fix]"
    fi

    set +e
    if [[ ${servicesString:2:4} == "all" || "${servicesString:2}" == *frontend* ]]; then
        echo -e "${CYAN}Running frontend lint...${NC}"
        # shellcheck disable=SC2086
        ${dockerCompose} exec -T frontend npm run lint -- ${cache} ${fix}
        frontendExitValue=$?
    fi
    if [[ ${servicesString:2:4} == "all" || "${servicesString:2}" == *backend* ]]; then
        echo -e "${CYAN}Running backend lint...${NC}"
        # shellcheck disable=SC2086
        ${dockerCompose} exec -T backend npm run lint -- ${cache} ${fix}
        backendExitValue=$?
    fi
    if [[ ${servicesString:2:4} == "all" || "${servicesString:2}" == *docs* ]]; then
        echo -e "${CYAN}Running docs lint...${NC}"
        # shellcheck disable=SC2086
        ${docker} run --rm -v "${scriptLocation}":/workdir ghcr.io/igorshubovych/markdownlint-cli:latest ".wiki" "*.md" ${fix}
        docsExitValue=$?
    fi
    if [[ ${servicesString:2:4} == "all" || "${servicesString:2}" == *shell* ]]; then
        echo -e "${CYAN}Running shell lint...${NC}"
        ${docker} run --rm -v "${scriptLocation}":/mnt koalaman/shellcheck:stable ./*.sh ./**/*.sh
        shellExitValue=$?
    fi
    set -e
    if [[ ${frontendExitValue} -gt 0 || ${backendExitValue} -gt 0 || ${docsExitValue} -gt 0 || ${shellExitValue} -gt 0 ]]; then
        exit 1
    fi
}

# Validate typescript in services
handleTypescript()
{
    set +e
    # shellcheck disable=SC2001
    services=$(sed "s/\(\ \)\{0,1\}\(-\)\{0,2\}strict//g;t;q1" <<< "${@:2}")
    strictFound=$?
    if [[ $strictFound -eq 0 ]]; then
        strict="--strict"
        echo -e "${GREEN}Strict mode enabled${NC}"
    fi
    set -e

    # shellcheck disable=SC2068
    servicesString=$(handleServices "backend frontend" ${services[@]})
    if [[ ${servicesString:0:1} != 1 ]]; then
        throw "${servicesString:2}\n${YELLOW}Usage: ${1} [backend, frontend] [strict]"
    fi

    set +e
    if [[ ${servicesString:2:4} == "all" || "${servicesString:2}" == *frontend* ]]; then
        echo -e "${CYAN}Running frontend typescript check...${NC}"
        # shellcheck disable=SC2086
        ${dockerCompose} exec -T frontend npm run typescript -- ${strict}
        frontendExitValue=$?
    fi
    if [[ ${servicesString:2:4} == "all" || "${servicesString:2}" == *backend* ]]; then
        echo -e "${CYAN}Running backend typescript check...${NC}"
        # shellcheck disable=SC2086
        ${dockerCompose} exec -T backend npm run typescript -- ${strict}
        backendExitValue=$?
    fi
    set -e
    if [[ ${frontendExitValue} -gt 0 || ${backendExitValue} -gt 0 ]]; then
        exit 1
    fi
}

# Execute automated tests in services
handleTests()
{
    # shellcheck disable=SC2068
    servicesString=$(handleServices "frontend" ${services[@]})
    if [[ ${servicesString:0:1} != 1 ]]; then
        throw "${servicesString:2}\n${YELLOW}Usage: ${1} [frontend]"
    fi

    set +e
    if [[ ${servicesString:2:4} == "all" || "${servicesString:2}" == *frontend* ]]; then
        echo -e "${CYAN}Running frontend tests...${NC}"
        ${dockerCompose} exec -T frontend npm run test -- --run
        frontendExitValue=$?
    fi
    set -e
    if [[ ${frontendExitValue} -gt 0 ]]; then
        exit 1
    fi
}

# Execute test coverage in services
handleTestCoverage()
{
    servicesString=$(handleServices "frontend" "${@:2}")
    if [[ ${servicesString:0:1} != 1 ]]; then
        throw "${servicesString:2}\n${YELLOW}Usage: ${1} [frontend]"
    fi

    set +e
    if [[ ${servicesString:2:4} == "all" || "${servicesString:2}" == *frontend* ]]; then
        echo -e "${CYAN}Running frontend test coverage report...${NC}"
        ${dockerCompose} exec -T frontend npm run coverage
        frontendExitValue=$?
    fi
    set -e
    if [[ ${frontendExitValue} -gt 0 ]]; then
        exit 1
    fi
}

# Update Musare
handleUpdate()
{
    musareshModified=$(git diff HEAD -- musare.sh)

    git fetch

    updated=$(git log --name-only --oneline HEAD..@\{u\})
    if [[ ${updated} == "" ]]; then
        echo -e "${GREEN}Already up to date${NC}"
        exit 0
    fi

    set +e
    breakingConfigChange=$(git rev-list "$(git rev-parse HEAD)" | grep d8b73be1de231821db34c677110b7b97e413451f)
    set -e
    if [[ -f backend/config/default.json && -z $breakingConfigChange ]]; then
        throw "Configuration has breaking changes. Please rename or remove 'backend/config/default.json' and run the update command again to continue."
    fi

    set +e
    musareshChange=$(echo "${updated}" | grep "musare.sh")
    dbChange=$(echo "${updated}" | grep "backend/logic/db/schemas")
    bcChange=$(echo "${updated}" | grep "backend/config/default.json")
    envChange=$(echo "${updated}" | grep ".env.example")
    set -e
    if [[ ( $2 == "auto" && -z $dbChange && -z $bcChange && -z $musareshChange && -z $envChange ) || -z $2 ]]; then
        if [[ -n $musareshChange && $(git diff @\{u\} -- musare.sh) != "" ]]; then
            if [[ $musareshModified != "" ]]; then
                throw "musare.sh has been modified, please reset these changes and run the update command again to continue."
            else
                git checkout @\{u\} -- musare.sh
                throw "${YELLOW}musare.sh has been updated, please run the update command again to continue."
            fi
        else
            git pull
            echo -e "${CYAN}Updating...${NC}"
            runDockerCommand "$(basename "$0") $1" pull
            runDockerCommand "$(basename "$0") $1" build
            runDockerCommand "$(basename "$0") $1" restart
            echo -e "${GREEN}Updated!${NC}"
            if [[ -n $dbChange ]]; then
                echo -e "${RED}Database schema has changed, please run migration!${NC}"
            fi
            if [[ -n $bcChange ]]; then
                echo -e "${RED}Backend config has changed, please update!${NC}"
            fi
            if [[ -n $envChange ]]; then
                echo -e "${RED}Environment config has changed, please update!${NC}"
            fi
        fi
    elif [[ $2 == "auto" ]]; then
        throw "Auto Update Failed! musare.sh, database and/or config has changed!"
    fi
}

# Backup the database
handleBackup()
{
    if [[ -z "${BACKUP_LOCATION}" ]]; then
        backupLocation="${scriptLocation%x}/backups"
    else
        backupLocation="${BACKUP_LOCATION%/}"
    fi
    if [[ ! -d "${backupLocation}" ]]; then
        echo -e "${YELLOW}Creating backup directory at ${backupLocation}${NC}"
        mkdir "${backupLocation}"
    fi
    if [[ -z "${BACKUP_NAME}" ]]; then
        backupLocation="${backupLocation}/musare-$(date +"%Y-%m-%d-%s").dump"
    else
        backupLocation="${backupLocation}/${BACKUP_NAME}"
    fi
    echo -e "${YELLOW}Creating backup at ${backupLocation}${NC}"
    ${dockerCompose} exec -T mongo sh -c "mongodump --authenticationDatabase musare -u ${MONGO_USER_USERNAME} -p ${MONGO_USER_PASSWORD} -d musare --archive" > "${backupLocation}"
}

# Restore database from dump
handleRestore()
{
    if [[ -z $2 ]]; then
        echo -e "${GREEN}Please enter the full path of the dump you wish to restore: ${NC}"
        read -r restoreFile
    else
        restoreFile=$2
    fi

    if [[ -z ${restoreFile} ]]; then
        throw "Error: no restore path given, cancelled restoration."
    elif [[ -d ${restoreFile} ]]; then
        throw "Error: restore path given is a directory, cancelled restoration."
    elif [[ ! -f ${restoreFile} ]]; then
        throw "Error: no file at restore path given, cancelled restoration."
    else
        ${dockerCompose} exec -T mongo sh -c "mongorestore --authenticationDatabase musare -u ${MONGO_USER_USERNAME} -p ${MONGO_USER_PASSWORD} --archive" < "${restoreFile}"
    fi
}

# Toggle user admin role
handleAdmin()
{
    MONGO_VERSION_INT=${MONGO_VERSION:0:1}

    case $2 in
        add)
            if [[ -z $3 ]]; then
                echo -e "${GREEN}Please enter the username of the user you wish to make an admin: ${NC}"
                read -r adminUser
            else
                adminUser=$3
            fi
            if [[ -z $adminUser ]]; then
                throw "Error: Username for new admin not provided."
            fi

            if [[ $MONGO_VERSION_INT -ge 5 ]]; then
                ${dockerCompose} exec mongo mongosh musare -u "${MONGO_USER_USERNAME}" -p "${MONGO_USER_PASSWORD}" --eval "disableTelemetry(); db.users.updateOne({username: '${adminUser}'}, {\$set: {role: 'admin'}})"
            else
                ${dockerCompose} exec mongo mongo musare -u "${MONGO_USER_USERNAME}" -p "${MONGO_USER_PASSWORD}" --eval "db.users.updateOne({username: '${adminUser}'}, {\$set: {role: 'admin'}})"
            fi
            ;;
        remove)
            if [[ -z $3 ]]; then
                echo -e "${GREEN}Please enter the username of the user you wish to remove as admin: ${NC}"
                read -r adminUser
            else
                adminUser=$3
            fi
            if [[ -z $adminUser ]]; then
                throw "Error: Username for new admin not provided."
            fi

            if [[ $MONGO_VERSION_INT -ge 5 ]]; then
                ${dockerCompose} exec mongo mongosh musare -u "${MONGO_USER_USERNAME}" -p "${MONGO_USER_PASSWORD}" --eval "disableTelemetry(); db.users.updateOne({username: '${adminUser}'}, {\$set: {role: 'default'}})"
            else
                ${dockerCompose} exec mongo mongo musare -u "${MONGO_USER_USERNAME}" -p "${MONGO_USER_PASSWORD}" --eval "db.users.updateOne({username: '${adminUser}'}, {\$set: {role: 'default'}})"
            fi
            ;;
        *)
            throw "Invalid command $2\n${YELLOW}Usage: ${1} [add,remove] username"
            ;;
    esac
}

availableCommands=$(cat << COMMANDS
start - Start services
stop - Stop services
restart - Restart services
status - Service status
logs - View logs for services
update - Update Musare
attach [backend,mongo,redis] - Attach to backend service, mongo or redis shell
build - Build services
lint - Run lint on frontend, backend, docs and/or shell
backup - Backup database data to file
restore - Restore database data from backup file
reset - Reset service data
admin [add,remove] - Assign/unassign admin role to/from a user
typescript - Run typescript checks on frontend and/or backend
COMMANDS
)

# Handle command
case $1 in
    start)
        echo -e "${CYAN}Musare | Start Services${NC}"
        # shellcheck disable=SC2068
        runDockerCommand "$(basename "$0") $1" start ${@:2}
        ;;

    stop)
        echo -e "${CYAN}Musare | Stop Services${NC}"
        # shellcheck disable=SC2068
        runDockerCommand "$(basename "$0") $1" stop ${@:2}
        ;;

    restart)
        echo -e "${CYAN}Musare | Restart Services${NC}"
        # shellcheck disable=SC2068
        runDockerCommand "$(basename "$0") $1" restart ${@:2}
        ;;

    build)
        echo -e "${CYAN}Musare | Build Services${NC}"
        # shellcheck disable=SC2068
        runDockerCommand "$(basename "$0") $1" pull ${@:2}
        # shellcheck disable=SC2068
        runDockerCommand "$(basename "$0") $1" build ${@:2}
        ;;

    status)
        echo -e "${CYAN}Musare | Service Status${NC}"
        # shellcheck disable=SC2068
        runDockerCommand "$(basename "$0") $1" ps ${@:2}
        ;;

    reset)
        echo -e "${CYAN}Musare | Reset Services${NC}"
        # shellcheck disable=SC2068
        handleReset "$(basename "$0") $1" ${@:2}
        ;;

    attach)
        echo -e "${CYAN}Musare | Attach${NC}"
        attachContainer "$(basename "$0") $1" "$2"
        ;;

    lint|eslint)
        echo -e "${CYAN}Musare | Lint${NC}"
        # shellcheck disable=SC2068
        handleLinting "$(basename "$0") $1" ${@:2}
        ;;

    typescript|ts)
        echo -e "${CYAN}Musare | TypeScript Check${NC}"
        # shellcheck disable=SC2068
        handleTypescript "$(basename "$0") $1" ${@:2}
        ;;

    test)
        echo -e "${CYAN}Musare | Test${NC}"
        # shellcheck disable=SC2068
        handleTests "$(basename "$0") $1" ${@:2}
        ;;

    test:coverage)
        echo -e "${CYAN}Musare | Test Coverage${NC}"
        # shellcheck disable=SC2068
        handleTestCoverage "$(basename "$0") $1" ${@:2}
        ;;

    update)
        echo -e "${CYAN}Musare | Update${NC}"
        # shellcheck disable=SC2068
        handleUpdate "$(basename "$0") $1" ${@:2}
        ;;

    logs)
        echo -e "${CYAN}Musare | Logs${NC}"
        # shellcheck disable=SC2068
        runDockerCommand "$(basename "$0") $1" logs ${@:2}
        ;;

    backup)
        echo -e "${CYAN}Musare | Backup${NC}"
        # shellcheck disable=SC2068
        handleBackup "$(basename "$0") $1" ${@:2}
        ;;

    restore)
        echo -e "${CYAN}Musare | Restore${NC}"
        # shellcheck disable=SC2068
        handleRestore "$(basename "$0") $1" ${@:2}
        ;;

    admin)
        echo -e "${CYAN}Musare | Add Admin${NC}"
        # shellcheck disable=SC2068
        handleAdmin "$(basename "$0") $1" ${@:2}
        ;;

    "")
        echo -e "${CYAN}Musare | Available Commands${NC}"
        echo -e "${YELLOW}${availableCommands}${NC}"
        ;;

    *)
        echo -e "${CYAN}Musare${NC}"
        echo -e "${RED}Error: Invalid Command $1${NC}"
        echo -e "${CYAN}Available Commands:${NC}"
        echo -e "${YELLOW}${availableCommands}${NC}"
        exit 1
        ;;
esac