#!/usr/bin/env bash

set -Eeuo pipefail

trap 'echo "[crm25-installer] Error on line ${LINENO}: ${BASH_COMMAND}" >&2' ERR

APP_SLUG="crm25"
APP_NAME="CRM25"
INSTALL_ROOT="/var/www/${APP_SLUG}"
APP_DIR="${INSTALL_ROOT}/current"
TMP_DIR="/tmp/${APP_SLUG}-install"
SITE_CONF="/etc/nginx/sites-available/${APP_SLUG}.conf"
SITE_LINK="/etc/nginx/sites-enabled/${APP_SLUG}.conf"
CRON_FILE="/etc/cron.d/${APP_SLUG}"
CREDENTIALS_FILE="/root/${APP_SLUG}-install-credentials.txt"
INSTALL_LOG_FILE="/var/log/${APP_SLUG}-installer.log"
DEFAULT_FEED_URL="https://update.crm25.webnet.kz/api/v1/crm25/updates?product=crm25&channel=stable&version=0.0.0"
PACKAGE_ZIP="${TMP_DIR}/${APP_SLUG}.zip"
NODESETUP_URL="https://deb.nodesource.com/setup_22.x"
PHP_TARGET_VERSION="8.4"
PHP_PACKAGE_PREFIX="php${PHP_TARGET_VERSION}"
PHP_BIN="/usr/bin/php${PHP_TARGET_VERSION}"
COMPOSER_BIN="/usr/bin/composer"
TOTAL_STEPS=20
CURRENT_STEP=0
PROGRESS_BAR_WIDTH=28
ANSI_RESET=""
ANSI_CYAN=""
ANSI_GREEN=""
ANSI_DIM=""

setup_output_style() {
    if [[ -t 1 ]]; then
        ANSI_RESET=$'\033[0m'
        ANSI_CYAN=$'\033[36m'
        ANSI_GREEN=$'\033[32m'
        ANSI_DIM=$'\033[2m'
    fi
}

enable_install_logging() {
    mkdir -p "$(dirname "${INSTALL_LOG_FILE}")"
    touch "${INSTALL_LOG_FILE}"
    chmod 0600 "${INSTALL_LOG_FILE}"

    exec > >(tee -a "${INSTALL_LOG_FILE}") 2>&1
}

progress_bar() {
    local percent="${1:-0}"
    local filled=0
    local empty=0
    local done_bar=""
    local todo_bar=""

    if (( percent < 0 )); then
        percent=0
    elif (( percent > 100 )); then
        percent=100
    fi

    filled=$((percent * PROGRESS_BAR_WIDTH / 100))
    empty=$((PROGRESS_BAR_WIDTH - filled))

    printf -v done_bar '%*s' "${filled}" ''
    printf -v todo_bar '%*s' "${empty}" ''

    done_bar="${done_bar// /#}"
    todo_bar="${todo_bar// /-}"

    printf '%s%s' "${done_bar}" "${todo_bar}"
}

stage() {
    local title="${1:-}"
    local percent=0
    local bar=""

    CURRENT_STEP=$((CURRENT_STEP + 1))
    percent=$((CURRENT_STEP * 100 / TOTAL_STEPS))
    bar="$(progress_bar "${percent}")"

    printf '\n%s[crm25-installer]%s %s[%3d%%%s | %02d/%02d | %s]%s %s\n' \
        "${ANSI_GREEN}" "${ANSI_RESET}" \
        "${ANSI_CYAN}" "${percent}" "${ANSI_RESET}" \
        "${CURRENT_STEP}" "${TOTAL_STEPS}" "${bar}" \
        "${ANSI_RESET}" "${title}"
}

run_stage() {
    local title="${1:-}"
    shift

    stage "${title}"
    "$@"
}

process_is_active() {
    local pid="${1:-0}"
    local state=""

    if ! kill -0 "${pid}" 2>/dev/null; then
        return 1
    fi

    state="$(
        set +o pipefail
        ps -o stat= -p "${pid}" 2>/dev/null | head -n 1 | tr -d '[:space:]'
    )"
    [[ -n "${state}" && "${state#Z}" == "${state}" ]]
}

run_with_heartbeat() {
    local title="${1:-}"
    local heartbeat_seconds="${2:-10}"
    local command_log_file="${3:-}"
    shift 3

    local start_ts=0
    local now_ts=0
    local elapsed=0
    local pid=0

    if [[ -n "${command_log_file}" ]]; then
        mkdir -p "$(dirname "${command_log_file}")"
        : >"${command_log_file}"
        "$@" > >(tee -a "${command_log_file}") 2>&1 &
    else
        "$@" &
    fi
    pid=$!
    start_ts="$(date +%s)"

    while process_is_active "${pid}"; do
        sleep "${heartbeat_seconds}"
        if process_is_active "${pid}"; then
            now_ts="$(date +%s)"
            elapsed=$((now_ts - start_ts))
            if [[ -n "${command_log_file}" ]]; then
                log "${title} is still running (${elapsed}s elapsed). Log: ${command_log_file}"
            else
                log "${title} is still running (${elapsed}s elapsed)."
            fi
        fi
    done

    wait "${pid}"
}

log() {
    printf '[crm25-installer] %s\n' "$1"
}

fail() {
    printf '[crm25-installer] %s\n' "$1" >&2
    exit 1
}

require_root() {
    if [[ "${EUID}" -ne 0 ]]; then
        fail "Run the installer as root."
    fi
}

require_apt() {
    if ! command -v apt-get >/dev/null 2>&1; then
        fail "Only Debian/Ubuntu-based servers with apt are supported."
    fi
}

is_package_installed() {
    local package_name="${1:-}"
    dpkg-query -W -f='${Status}' "${package_name}" 2>/dev/null | grep -q 'install ok installed'
}

php_runtime_available() {
    local candidate=""

    candidate="$(apt-cache policy "${PHP_PACKAGE_PREFIX}-cli" 2>/dev/null | awk '/Candidate:/ {print $2; exit}')"
    [[ -n "${candidate}" && "${candidate}" != "(none)" ]]
}

ensure_php_runtime_repository() {
    local distro_id=""
    local distro_codename=""

    if php_runtime_available; then
        return 0
    fi

    log "PHP ${PHP_TARGET_VERSION} packages are not available in the current apt sources. Adding a PHP repository."

    apt-get install -y ca-certificates curl gnupg lsb-release software-properties-common

    distro_id="$(. /etc/os-release && printf '%s' "${ID:-}")"
    distro_codename="$(. /etc/os-release && printf '%s' "${VERSION_CODENAME:-}")"

    if [[ "${distro_id}" == "ubuntu" ]]; then
        LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php
    elif [[ "${distro_id}" == "debian" ]]; then
        install -d -m 0755 /etc/apt/keyrings
        curl -fsSL https://packages.sury.org/php/apt.gpg | gpg --dearmor -o /etc/apt/keyrings/php.gpg
        printf 'deb [signed-by=/etc/apt/keyrings/php.gpg] https://packages.sury.org/php/ %s main\n' "${distro_codename}" >/etc/apt/sources.list.d/php.list
    else
        fail "Automatic PHP ${PHP_TARGET_VERSION} repository setup is supported only on Ubuntu and Debian."
    fi

    apt-get update

    if ! php_runtime_available; then
        fail "PHP ${PHP_TARGET_VERSION} packages are still unavailable after adding the repository."
    fi
}

can_use_systemctl() {
    command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]
}

systemd_unit_exists() {
    local service_name="${1:-}"

    if [[ -z "${service_name}" ]] || ! can_use_systemctl; then
        return 1
    fi

    if systemctl list-unit-files --type=service --all --no-legend --no-pager 2>/dev/null | awk '{print $1}' | grep -Fxq "${service_name}.service"; then
        return 0
    fi

    if systemctl status "${service_name}.service" >/dev/null 2>&1; then
        return 0
    fi

    [[ -f "/etc/systemd/system/${service_name}.service" || -f "/lib/systemd/system/${service_name}.service" || -f "/usr/lib/systemd/system/${service_name}.service" ]]
}

find_service_name() {
    local service_name=""

    for service_name in "$@"; do
        if systemd_unit_exists "${service_name}"; then
            printf '%s' "${service_name}"
            return 0
        fi
        if command -v service >/dev/null 2>&1 && service "${service_name}" status >/dev/null 2>&1; then
            printf '%s' "${service_name}"
            return 0
        fi
        if [[ -x "/etc/init.d/${service_name}" ]]; then
            printf '%s' "${service_name}"
            return 0
        fi
    done

    return 1
}

start_service() {
    local service_name="${1:-}"

    if [[ -z "${service_name}" ]]; then
        return 1
    fi

    if can_use_systemctl && systemd_unit_exists "${service_name}"; then
        systemctl enable --now "${service_name}"
        return 0
    fi

    if command -v service >/dev/null 2>&1 && service "${service_name}" start >/dev/null 2>&1; then
        return 0
    fi

    if [[ -x "/etc/init.d/${service_name}" ]]; then
        "/etc/init.d/${service_name}" start
        return 0
    fi

    return 1
}

restart_service() {
    local service_name="${1:-}"

    if [[ -z "${service_name}" ]]; then
        return 1
    fi

    if can_use_systemctl && systemd_unit_exists "${service_name}"; then
        systemctl restart "${service_name}"
        return 0
    fi

    if command -v service >/dev/null 2>&1 && service "${service_name}" restart >/dev/null 2>&1; then
        return 0
    fi

    if [[ -x "/etc/init.d/${service_name}" ]]; then
        "/etc/init.d/${service_name}" restart
        return 0
    fi

    return 1
}

reload_service() {
    local service_name="${1:-}"

    if [[ -z "${service_name}" ]]; then
        return 1
    fi

    if can_use_systemctl && systemd_unit_exists "${service_name}"; then
        systemctl reload "${service_name}"
        return 0
    fi

    if command -v service >/dev/null 2>&1 && service "${service_name}" reload >/dev/null 2>&1; then
        return 0
    fi

    if [[ -x "/etc/init.d/${service_name}" ]]; then
        "/etc/init.d/${service_name}" reload
        return 0
    fi

    restart_service "${service_name}"
}

detect_redis_config() {
    local candidate=""

    for candidate in \
        /etc/redis/redis.conf \
        /etc/redis/redis-server.conf \
        /etc/redis/redis-server/redis.conf; do
        if [[ -f "${candidate}" ]]; then
            REDIS_CONF="${candidate}"
            return 0
        fi
    done

    if [[ -n "${REDIS_SERVICE:-}" ]]; then
        for candidate in \
            "/etc/redis/${REDIS_SERVICE}.conf" \
            "/etc/${REDIS_SERVICE}.conf"; do
            if [[ -f "${candidate}" ]]; then
                REDIS_CONF="${candidate}"
                return 0
            fi
        done
    fi

    return 1
}

random_alnum() {
    local length="${1:-24}"
    local value=""
    local chunk=""

    while [[ "${#value}" -lt "${length}" ]]; do
        chunk="$(
            set +o pipefail
            LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c "$((length - ${#value}))"
        )"
        value+="${chunk}"
    done

    printf '%s' "${value:0:length}"
}

trim() {
    local value="${1:-}"
    value="${value#"${value%%[![:space:]]*}"}"
    value="${value%"${value##*[![:space:]]}"}"
    printf '%s' "${value}"
}

is_ip_address() {
    local host="${1:-}"

    if [[ ! "${host}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
        return 1
    fi

    IFS='.' read -r -a octets <<<"${host}"
    for octet in "${octets[@]}"; do
        if (( octet < 0 || octet > 255 )); then
            return 1
        fi
    done

    return 0
}

resolve_public_ip() {
    local detected_ip=""

    detected_ip="$(curl -4fsS --max-time 5 https://api.ipify.org 2>/dev/null || true)"
    if [[ -z "${detected_ip}" ]]; then
        detected_ip="$(curl -4fsS --max-time 5 https://ifconfig.me 2>/dev/null || true)"
    fi
    if [[ -z "${detected_ip}" ]]; then
        detected_ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
    fi

    detected_ip="$(trim "${detected_ip}")"
    if [[ -z "${detected_ip}" ]] || ! is_ip_address "${detected_ip}"; then
        fail "Could not determine the public IP address. Set CRM25_DOMAIN manually."
    fi

    printf '%s' "${detected_ip}"
}

normalize_host() {
    local value
    value="$(trim "${1:-}")"
    value="${value#http://}"
    value="${value#https://}"
    value="${value%%/*}"
    value="${value%%:*}"
    printf '%s' "${value}"
}

prompt_text() {
    local prompt="${1:-}"
    local __result_var="${2:-REPLY}"
    local value=""

    if { exec 3<>/dev/tty; } 2>/dev/null; then
        printf '%s' "${prompt}" >&3
        if ! IFS= read -r value <&3; then
            value=""
        fi
        exec 3<&-
        exec 3>&-
    elif [[ -t 0 ]]; then
        if ! IFS= read -r -p "${prompt}" value; then
            value=""
        fi
    else
        value=""
    fi

    printf -v "${__result_var}" '%s' "${value}"
}

prompt_for_host() {
    local raw_host="${CRM25_DOMAIN:-}"
    local default_ip=""

    if [[ -z "${raw_host}" ]]; then
        default_ip="$(resolve_public_ip)"
        prompt_text "Domain for CRM25 (press Enter to use ${default_ip}): " raw_host
    fi

    raw_host="$(normalize_host "${raw_host}")"
    if [[ -z "${raw_host}" ]]; then
        if [[ -z "${default_ip}" ]]; then
            default_ip="$(resolve_public_ip)"
        fi
        raw_host="${default_ip}"
    fi

    SITE_HOST="${raw_host}"
    if is_ip_address "${SITE_HOST}"; then
        USE_TLS="0"
        APP_SCHEME="http"
        WEB_PORT="80"
    else
        USE_TLS="1"
        APP_SCHEME="https"
        WEB_PORT="443"
    fi

    APP_URL="${APP_SCHEME}://${SITE_HOST}"
}

log_target_summary() {
    log "Target host: ${SITE_HOST}"
    log "Application URL: ${APP_URL}"
    log "Required PHP runtime: ${PHP_TARGET_VERSION}"
    log "Credentials file: ${CREDENTIALS_FILE}"
    log "Installer log: ${INSTALL_LOG_FILE}"
}

log_runtime_versions() {
    local php_version=""
    local node_version=""
    local npm_version=""
    local mysql_version=""
    local redis_version=""

    php_version="$("${PHP_BIN}" -v 2>/dev/null | head -n 1 || true)"
    node_version="$(node -v 2>/dev/null || true)"
    npm_version="$(npm -v 2>/dev/null || true)"
    mysql_version="$(mysql --version 2>/dev/null | head -n 1 || true)"
    redis_version="$(redis-server --version 2>/dev/null | head -n 1 || true)"

    [[ -n "${php_version}" ]] && log "PHP runtime: ${php_version}"
    [[ -n "${node_version}" ]] && log "Node.js runtime: ${node_version}"
    [[ -n "${npm_version}" ]] && log "npm runtime: ${npm_version}"
    [[ -n "${mysql_version}" ]] && log "MySQL runtime: ${mysql_version}"
    [[ -n "${redis_version}" ]] && log "Redis runtime: ${redis_version}"
}

ensure_node_runtime() {
    if command -v node >/dev/null 2>&1 && node -v | grep -Eq '^v(22|23|24)\.'; then
        return 0
    fi

    log "Installing current Node.js runtime from NodeSource."
    curl -fsSL "${NODESETUP_URL}" | bash -
    apt-get install -y nodejs
}

install_system_packages() {
    log "Installing system packages."

    export DEBIAN_FRONTEND=noninteractive
    apt-get update
    ensure_php_runtime_repository
    apt-get install -y \
        ca-certificates \
        cron \
        curl \
        git \
        gnupg \
        lsb-release \
        default-mysql-server \
        nginx \
        redis-server \
        software-properties-common \
        supervisor \
        unzip \
        zip \
        build-essential \
        "${PHP_PACKAGE_PREFIX}" \
        "${PHP_PACKAGE_PREFIX}-bcmath" \
        "${PHP_PACKAGE_PREFIX}-cli" \
        "${PHP_PACKAGE_PREFIX}-curl" \
        "${PHP_PACKAGE_PREFIX}-fpm" \
        "${PHP_PACKAGE_PREFIX}-gd" \
        "${PHP_PACKAGE_PREFIX}-intl" \
        "${PHP_PACKAGE_PREFIX}-mbstring" \
        "${PHP_PACKAGE_PREFIX}-mysql" \
        "${PHP_PACKAGE_PREFIX}-redis" \
        "${PHP_PACKAGE_PREFIX}-sqlite3" \
        "${PHP_PACKAGE_PREFIX}-xml" \
        "${PHP_PACKAGE_PREFIX}-zip" \
        composer

    if [[ -x "/usr/bin/php${PHP_TARGET_VERSION}" ]]; then
        PHP_BIN="/usr/bin/php${PHP_TARGET_VERSION}"
    elif command -v "php${PHP_TARGET_VERSION}" >/dev/null 2>&1; then
        PHP_BIN="$(command -v "php${PHP_TARGET_VERSION}")"
    fi

    COMPOSER_BIN="$(command -v composer || printf '%s' "${COMPOSER_BIN}")"

    if command -v update-alternatives >/dev/null 2>&1; then
        [[ -x "${PHP_BIN}" ]] && update-alternatives --set php "${PHP_BIN}" || true
        [[ -x "/usr/bin/phar${PHP_TARGET_VERSION}" ]] && update-alternatives --set phar "/usr/bin/phar${PHP_TARGET_VERSION}" || true
        [[ -x "/usr/bin/phar.phar${PHP_TARGET_VERSION}" ]] && update-alternatives --set phar.phar "/usr/bin/phar.phar${PHP_TARGET_VERSION}" || true
        [[ -x "/usr/bin/phpize${PHP_TARGET_VERSION}" ]] && update-alternatives --set phpize "/usr/bin/phpize${PHP_TARGET_VERSION}" || true
        [[ -x "/usr/bin/php-config${PHP_TARGET_VERSION}" ]] && update-alternatives --set php-config "/usr/bin/php-config${PHP_TARGET_VERSION}" || true
    fi

    if [[ "${USE_TLS}" == "1" ]]; then
        apt-get install -y certbot python3-certbot-nginx
    fi
}

detect_runtime_services() {
    if [[ -S "/run/php/${PHP_PACKAGE_PREFIX}-fpm.sock" ]]; then
        PHP_FPM_SOCKET="/run/php/${PHP_PACKAGE_PREFIX}-fpm.sock"
    else
        PHP_FPM_SOCKET="$(find /run/php -maxdepth 1 -type s -name 'php*-fpm.sock' | head -n 1 || true)"
    fi
    if [[ -z "${PHP_FPM_SOCKET}" ]]; then
        fail "PHP-FPM socket was not found."
    fi

    PHP_FPM_SERVICE="$(basename "${PHP_FPM_SOCKET}" .sock)"

    if [[ -x "/usr/bin/php${PHP_TARGET_VERSION}" ]]; then
        PHP_BIN="/usr/bin/php${PHP_TARGET_VERSION}"
    elif command -v "php${PHP_TARGET_VERSION}" >/dev/null 2>&1; then
        PHP_BIN="$(command -v "php${PHP_TARGET_VERSION}")"
    elif command -v php >/dev/null 2>&1; then
        PHP_BIN="$(command -v php)"
    else
        fail "PHP CLI binary was not found."
    fi

    COMPOSER_BIN="$(command -v composer || printf '%s' "${COMPOSER_BIN}")"

    if can_use_systemctl; then
        systemctl daemon-reload >/dev/null 2>&1 || true
    fi

    MYSQL_SERVICE="$(find_service_name mysql mariadb mysqld || true)"
    if [[ -z "${MYSQL_SERVICE}" ]] && command -v mysql >/dev/null 2>&1; then
        MYSQL_SERVICE="mysql"
    fi
    if [[ -z "${MYSQL_SERVICE}" ]]; then
        fail "MySQL/MariaDB service was not found."
    fi

    if ! is_package_installed redis-server; then
        log "Redis package is missing. Installing redis-server."
        apt-get install -y redis-server
        if can_use_systemctl; then
            systemctl daemon-reload >/dev/null 2>&1 || true
        fi
    fi

    REDIS_SERVICE="$(find_service_name redis-server redis || true)"
    if [[ -z "${REDIS_SERVICE}" ]] && command -v redis-server >/dev/null 2>&1; then
        REDIS_SERVICE="redis-server"
    fi
    if [[ -z "${REDIS_SERVICE}" ]]; then
        fail "Redis service was not found."
    fi

    if ! detect_redis_config; then
        fail "Redis configuration file was not found."
    fi
}

enable_core_services() {
    log "Starting infrastructure services."

    start_service "${MYSQL_SERVICE}" || fail "Could not start MySQL/MariaDB service ${MYSQL_SERVICE}."
    start_service "${REDIS_SERVICE}" || fail "Could not start Redis service ${REDIS_SERVICE}."
    start_service "${PHP_FPM_SERVICE}" || fail "Could not start PHP-FPM service ${PHP_FPM_SERVICE}."
    start_service nginx || fail "Could not start Nginx."
    start_service supervisor || fail "Could not start Supervisor."
    start_service cron || fail "Could not start cron."
}

fetch_latest_release() {
    local feed_url="${CRM25_FEED_URL:-${DEFAULT_FEED_URL}}"
    local payload=""

    mkdir -p "${TMP_DIR}"

    log "Fetching the latest CRM25 release metadata."
    payload="$(curl -fsSL "${feed_url}")"

    PACKAGE_URL="$("${PHP_BIN}" -r '$payload = json_decode(stream_get_contents(STDIN), true); if (!is_array($payload)) { fwrite(STDERR, "Invalid update feed.\n"); exit(1); } $url = trim((string)($payload["download_url"] ?? "")); $version = trim((string)($payload["latest_version"] ?? $payload["version"] ?? "")); if ($url === "" || $version === "") { fwrite(STDERR, "Feed is missing download_url or version.\n"); exit(1); } echo $url, "\n", $version;' <<<"${payload}" | sed -n '1p')"
    RELEASE_VERSION="$("${PHP_BIN}" -r '$payload = json_decode(stream_get_contents(STDIN), true); echo trim((string)($payload["latest_version"] ?? $payload["version"] ?? ""));' <<<"${payload}")"

    if [[ -z "${PACKAGE_URL}" ]] || [[ -z "${RELEASE_VERSION}" ]]; then
        fail "Update feed did not return a valid release."
    fi

    log "Downloading CRM25 ${RELEASE_VERSION}."
    curl -fL "${PACKAGE_URL}" -o "${PACKAGE_ZIP}"
}

prepare_application_files() {
    log "Preparing application directory."

    if [[ -f "${APP_DIR}/artisan" ]]; then
        if [[ "${CRM25_INSTALL_FORCE:-0}" == "1" ]]; then
            log "Existing application directory will be overwritten because CRM25_INSTALL_FORCE=1."
        elif [[ ! -f "${APP_DIR}/.env" || ! -f "${APP_DIR}/vendor/autoload.php" ]]; then
            log "Detected an incomplete previous installation in ${APP_DIR}. Cleaning it up automatically."
        else
            fail "Application directory ${APP_DIR} already exists. Set CRM25_INSTALL_FORCE=1 to overwrite."
        fi
    fi

    rm -rf "${APP_DIR}"
    mkdir -p "${APP_DIR}"
    unzip -q "${PACKAGE_ZIP}" -d "${APP_DIR}"

    find "${APP_DIR}" \( -name '.DS_Store' -o -name '._*' \) -delete
    rm -rf "${APP_DIR}/__MACOSX"

    mkdir -p \
        "${APP_DIR}/bootstrap/cache" \
        "${APP_DIR}/storage/app/private" \
        "${APP_DIR}/storage/app/public" \
        "${APP_DIR}/storage/framework/cache/data" \
        "${APP_DIR}/storage/framework/sessions" \
        "${APP_DIR}/storage/framework/testing" \
        "${APP_DIR}/storage/framework/views" \
        "${APP_DIR}/storage/logs"
}

configure_mysql() {
    DB_NAME="${CRM25_DB_NAME:-crm25}"
    DB_USER="${CRM25_DB_USER:-crm25}"
    DB_PASSWORD="${CRM25_DB_PASSWORD:-$(random_alnum 24)}"

    log "Configuring MySQL database."
    mysql --protocol=socket -uroot <<SQL
CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'127.0.0.1' IDENTIFIED BY '${DB_PASSWORD}';
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASSWORD}';
ALTER USER '${DB_USER}'@'127.0.0.1' IDENTIFIED BY '${DB_PASSWORD}';
ALTER USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASSWORD}';
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'127.0.0.1';
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
SQL
}

configure_redis() {
    REDIS_PASSWORD="${CRM25_REDIS_PASSWORD:-$(random_alnum 24)}"
    local redis_conf="${REDIS_CONF:-}"

    if [[ -z "${redis_conf}" ]] && ! detect_redis_config; then
        fail "Redis configuration file was not found."
    fi
    redis_conf="${REDIS_CONF}"

    log "Securing Redis for local-only access."

    sed -i.bak -E 's/^#?\s*bind\s+.*/bind 127.0.0.1 -::1/' "${redis_conf}"
    sed -i.bak -E 's/^#?\s*protected-mode\s+.*/protected-mode yes/' "${redis_conf}"
    sed -i.bak -E 's/^#?\s*port\s+.*/port 6379/' "${redis_conf}"

    if grep -Eq '^\s*#?\s*requirepass\s+' "${redis_conf}"; then
        sed -i.bak -E "s|^#?\\s*requirepass\\s+.*|requirepass ${REDIS_PASSWORD}|" "${redis_conf}"
    else
        printf '\nrequirepass %s\n' "${REDIS_PASSWORD}" >>"${redis_conf}"
    fi

    restart_service "${REDIS_SERVICE}" || fail "Could not restart Redis service ${REDIS_SERVICE}."
}

mysql_scalar() {
    local sql="${1:-}"
    mysql --protocol=socket -N -uroot -D "${DB_NAME}" -e "${sql}" 2>/dev/null | head -n 1 | tr -d '\r'
}

mysql_table_exists() {
    local table_name="${1:-}"
    local count="0"

    count="$(mysql_scalar "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '${table_name}'")"
    [[ "${count}" =~ ^[1-9][0-9]*$ ]]
}

mysql_migration_exists() {
    local migration_name="${1:-}"
    local count="0"

    if ! mysql_table_exists migrations; then
        return 1
    fi

    count="$(mysql_scalar "SELECT COUNT(*) FROM migrations WHERE migration = '${migration_name}'")"
    [[ "${count}" =~ ^[1-9][0-9]*$ ]]
}

should_rebuild_database_before_migrate() {
    local known_table_count="0"

    if [[ "${CRM25_INSTALL_FORCE:-0}" == "1" ]]; then
        known_table_count="$(mysql_scalar "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name IN ('users','companies','contacts','pipelines','deal_stages','deals','tasks','migrations')")"
        if [[ "${known_table_count}" =~ ^[1-9][0-9]*$ ]]; then
            return 0
        fi
    fi

    if mysql_table_exists deal_stages && ! mysql_migration_exists '2026_02_27_033106_create_deal_stages_table'; then
        return 0
    fi

    if mysql_table_exists deals && ! mysql_migration_exists '2026_02_27_033106_create_deals_table'; then
        return 0
    fi

    if mysql_table_exists pipelines && ! mysql_migration_exists '2026_02_27_033106_create_pipelines_table'; then
        return 0
    fi

    return 1
}

write_http_nginx_config() {
    cat >"${SITE_CONF}" <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${SITE_HOST};
    root ${APP_DIR}/public;

    index index.php index.html;
    client_max_body_size 50m;

    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;

    location / {
        try_files \$uri \$uri/ /index.php?\$query_string;
    }

    location /app {
        proxy_http_version 1.1;
        proxy_set_header Host \$host;
        proxy_set_header Scheme \$scheme;
        proxy_set_header SERVER_PORT \$server_port;
        proxy_set_header REMOTE_ADDR \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass http://127.0.0.1:8080;
    }

    location /apps {
        proxy_http_version 1.1;
        proxy_set_header Host \$host;
        proxy_set_header Scheme \$scheme;
        proxy_set_header SERVER_PORT \$server_port;
        proxy_set_header REMOTE_ADDR \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass http://127.0.0.1:8080;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:${PHP_FPM_SOCKET};
        fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}
EOF
}

write_https_nginx_config() {
    cat >"${SITE_CONF}" <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${SITE_HOST};
    return 301 https://\$host\$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${SITE_HOST};
    root ${APP_DIR}/public;

    index index.php index.html;
    client_max_body_size 50m;

    ssl_certificate /etc/letsencrypt/live/${SITE_HOST}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${SITE_HOST}/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;

    location / {
        try_files \$uri \$uri/ /index.php?\$query_string;
    }

    location /app {
        proxy_http_version 1.1;
        proxy_set_header Host \$host;
        proxy_set_header Scheme https;
        proxy_set_header SERVER_PORT 443;
        proxy_set_header REMOTE_ADDR \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass http://127.0.0.1:8080;
    }

    location /apps {
        proxy_http_version 1.1;
        proxy_set_header Host \$host;
        proxy_set_header Scheme https;
        proxy_set_header SERVER_PORT 443;
        proxy_set_header REMOTE_ADDR \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass http://127.0.0.1:8080;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:${PHP_FPM_SOCKET};
        fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}
EOF
}

configure_nginx() {
    log "Configuring Nginx."

    write_http_nginx_config
    ln -sfn "${SITE_CONF}" "${SITE_LINK}"
    rm -f /etc/nginx/sites-enabled/default
    nginx -t
    reload_service nginx || fail "Could not reload Nginx."
}

obtain_tls_certificate_if_needed() {
    if [[ "${USE_TLS}" != "1" ]]; then
        return
    fi

    local certbot_email="admin@${SITE_HOST}"
    log "Attempting to obtain a Let's Encrypt certificate for ${SITE_HOST}."

    if certbot --nginx --non-interactive --agree-tos --redirect -m "${certbot_email}" -d "${SITE_HOST}"; then
        write_https_nginx_config
        nginx -t
        reload_service nginx || fail "Could not reload Nginx after issuing TLS certificate."
        APP_SCHEME="https"
        WEB_PORT="443"
        APP_URL="${APP_SCHEME}://${SITE_HOST}"
        return
    fi

    log "TLS certificate was not issued. Continuing with HTTP."
    USE_TLS="0"
    APP_SCHEME="http"
    WEB_PORT="80"
    APP_URL="${APP_SCHEME}://${SITE_HOST}"
    write_http_nginx_config
    nginx -t
    reload_service nginx || fail "Could not reload Nginx after HTTP fallback."
}

write_env_file() {
    REVERB_APP_ID="${CRM25_REVERB_APP_ID:-$(date +%s)}"
    REVERB_APP_KEY="${CRM25_REVERB_APP_KEY:-$(random_alnum 24)}"
    REVERB_APP_SECRET="${CRM25_REVERB_APP_SECRET:-$(random_alnum 32)}"
    ADMIN_EMAIL="${CRM25_ADMIN_EMAIL:-}"
    if [[ -z "${ADMIN_EMAIL}" ]]; then
        if is_ip_address "${SITE_HOST}"; then
            ADMIN_EMAIL="admin@crm.local"
        else
            ADMIN_EMAIL="admin@${SITE_HOST}"
        fi
    fi
    ADMIN_PASSWORD="${CRM25_ADMIN_PASSWORD:-$(random_alnum 20)}"

    log "Writing .env configuration."

    cat >"${APP_DIR}/.env" <<EOF
APP_NAME=${APP_NAME}
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=${APP_URL}
APP_VERSION=${RELEASE_VERSION}

APP_LOCALE=ru
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=ru_RU

APP_MAINTENANCE_DRIVER=file

BCRYPT_ROUNDS=12

LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=warning

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=${DB_NAME}
DB_USERNAME=${DB_USER}
DB_PASSWORD=${DB_PASSWORD}

SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null

BROADCAST_CONNECTION=reverb
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database

CACHE_STORE=redis
HOT_CACHE_STORE=hot
HOT_CACHE_FAILOVER_STORES=redis,database,array

MEMCACHED_HOST=127.0.0.1

REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=${REDIS_PASSWORD}
REDIS_PORT=6379
REDIS_DB=0
REDIS_CACHE_DB=1
REDIS_TIMEOUT=2.0
REDIS_READ_TIMEOUT=2.0
REDIS_MAX_RETRIES=3
REDIS_BACKOFF_ALGORITHM=decorrelated_jitter
REDIS_BACKOFF_BASE=100
REDIS_BACKOFF_CAP=1000

MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS=${ADMIN_EMAIL}
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

REVERB_SERVER=reverb
REVERB_SERVER_HOST=127.0.0.1
REVERB_SERVER_PORT=8080
REVERB_SERVER_PATH=
REVERB_HOST=${SITE_HOST}
REVERB_PORT=${WEB_PORT}
REVERB_SCHEME=${APP_SCHEME}
REVERB_APP_ID=${REVERB_APP_ID}
REVERB_APP_KEY=${REVERB_APP_KEY}
REVERB_APP_SECRET=${REVERB_APP_SECRET}
REVERB_APP_PING_INTERVAL=60
REVERB_APP_ACTIVITY_TIMEOUT=30

VITE_APP_NAME="${APP_NAME}"
VITE_REVERB_APP_KEY=${REVERB_APP_KEY}
VITE_REVERB_HOST=${SITE_HOST}
VITE_REVERB_PORT=${WEB_PORT}
VITE_REVERB_SCHEME=${APP_SCHEME}

APP_UPDATE_TARGET_PATH=${APP_DIR}
APP_UPDATE_BACKUPS_ENABLED=true
APP_UPDATE_RUN_POST_COMMANDS=true
APP_UPDATE_BACKGROUND_LOG_PATH=${APP_DIR}/storage/logs/update-worker.log
EOF
}

install_php_and_frontend_dependencies() {
    cd "${APP_DIR}"
    local frontend_log="${APP_DIR}/storage/logs/frontend-build.log"
    local build_manifest="${APP_DIR}/public/build/manifest.json"

    if [[ ! -f vendor/autoload.php ]]; then
        log "Vendor directory is missing. Running composer install."
        run_with_heartbeat "Composer install" 10 "${APP_DIR}/storage/logs/composer-install.log" \
            env COMPOSER_ALLOW_SUPERUSER=1 "${PHP_BIN}" "${COMPOSER_BIN}" install --no-dev --prefer-dist --optimize-autoloader
    fi

    if [[ -f "${build_manifest}" ]]; then
        log "Prebuilt frontend assets were found in the release package. Skipping npm ci and Vite build."
        return
    fi

    ensure_node_runtime

    log "Installing frontend dependencies."
    run_with_heartbeat "npm ci" 10 "${APP_DIR}/storage/logs/npm-ci.log" \
        npm ci --no-audit --no-fund --progress=false

    log "Building frontend assets with Vite. This can take time on small servers."
    run_with_heartbeat "Vite build" 10 "${frontend_log}" \
        npm run build -- --logLevel info
    rm -rf node_modules
}

provision_application() {
    log "Running Laravel migrations and bootstrap provisioning."

    cd "${APP_DIR}"
    "${PHP_BIN}" artisan key:generate --force

    if [[ -L public/storage || -e public/storage ]]; then
        rm -rf public/storage
    fi
    "${PHP_BIN}" artisan storage:link || true

    if should_rebuild_database_before_migrate; then
        log "Detected partial MySQL schema from a previous install attempt. Rebuilding database with migrate:fresh."
        "${PHP_BIN}" artisan migrate:fresh --force
    else
        "${PHP_BIN}" artisan migrate --force
    fi

    CRM25_INSTALL_SITE_HOST="${SITE_HOST}" \
    CRM25_INSTALL_ADMIN_EMAIL="${ADMIN_EMAIL}" \
    CRM25_INSTALL_ADMIN_PASSWORD="${ADMIN_PASSWORD}" \
    "${PHP_BIN}" <<'PHP'
<?php

chdir(getenv('PWD') ?: __DIR__);

require getcwd().'/vendor/autoload.php';

$app = require getcwd().'/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();

$host = trim((string) getenv('CRM25_INSTALL_SITE_HOST'));
$adminEmail = trim((string) getenv('CRM25_INSTALL_ADMIN_EMAIL'));
$adminPassword = trim((string) getenv('CRM25_INSTALL_ADMIN_PASSWORD'));

if ($adminEmail === '') {
    $adminEmail = filter_var($host, FILTER_VALIDATE_IP) ? 'admin@crm.local' : 'admin@'.$host;
}
if ($adminPassword === '') {
    $adminPassword = 'password';
}

\App\Models\OrganizationSetting::current();

$admin = \App\Models\User::query()->firstOrNew([
    'email' => $adminEmail,
]);

$admin->fill([
    'name' => 'CRM Admin',
    'phone' => '+7-700-000-0000',
    'job_title' => 'CRM Administrator',
    'role' => 'admin',
    'locale' => 'ru',
    'email_verified_at' => now(),
    'permissions' => [],
]);

if (! $admin->exists) {
    $admin->password = \Illuminate\Support\Facades\Hash::make($adminPassword);
}

$admin->save();

$pipeline = \App\Models\Pipeline::query()->firstOrCreate(
    [
        'name' => 'Primary Sales',
    ],
    [
        'description' => 'Default sales pipeline created during server installation.',
        'creator_id' => $admin->id,
        'is_default' => true,
        'is_active' => true,
        'sort_order' => 0,
    ]
);

\App\Models\Pipeline::query()
    ->whereKeyNot($pipeline->id)
    ->where('is_default', true)
    ->update(['is_default' => false]);

$pipeline->forceFill([
    'creator_id' => $pipeline->creator_id ?: $admin->id,
    'is_default' => true,
    'is_active' => true,
    'sort_order' => 0,
])->save();

$stages = [
    ['code' => 'new_lead', 'name' => 'New Lead', 'probability' => 10, 'color' => '#94a3b8', 'is_won' => false, 'is_lost' => false],
    ['code' => 'qualification', 'name' => 'Qualification', 'probability' => 30, 'color' => '#3b82f6', 'is_won' => false, 'is_lost' => false],
    ['code' => 'proposal', 'name' => 'Proposal', 'probability' => 60, 'color' => '#8b5cf6', 'is_won' => false, 'is_lost' => false],
    ['code' => 'negotiation', 'name' => 'Negotiation', 'probability' => 80, 'color' => '#f59e0b', 'is_won' => false, 'is_lost' => false],
    ['code' => 'won', 'name' => 'Won', 'probability' => 100, 'color' => '#10b981', 'is_won' => true, 'is_lost' => false],
    ['code' => 'lost', 'name' => 'Lost', 'probability' => 0, 'color' => '#ef4444', 'is_won' => false, 'is_lost' => true],
];

foreach ($stages as $index => $stage) {
    \App\Models\DealStage::query()->updateOrCreate(
        [
            'pipeline_id' => $pipeline->id,
            'code' => $stage['code'],
        ],
        [
            'name' => $stage['name'],
            'sort_order' => $index,
            'probability' => $stage['probability'],
            'color' => $stage['color'],
            'is_won' => $stage['is_won'],
            'is_lost' => $stage['is_lost'],
        ]
    );
}
PHP

    "${PHP_BIN}" artisan optimize:clear
    "${PHP_BIN}" artisan config:cache
    "${PHP_BIN}" artisan route:cache
    "${PHP_BIN}" artisan view:cache
}

write_supervisor_configs() {
    log "Configuring Supervisor."

    cat >/etc/supervisor/conf.d/${APP_SLUG}-queue.conf <<EOF
[program:${APP_SLUG}-queue]
command=${PHP_BIN} ${APP_DIR}/artisan queue:work --sleep=1 --tries=3 --timeout=120
directory=${APP_DIR}
user=www-data
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=1
redirect_stderr=true
stdout_logfile=${APP_DIR}/storage/logs/queue-worker.log
stopwaitsecs=3600
EOF

    cat >/etc/supervisor/conf.d/${APP_SLUG}-reverb.conf <<EOF
[program:${APP_SLUG}-reverb]
command=${PHP_BIN} ${APP_DIR}/artisan reverb:start --host=127.0.0.1 --port=8080
directory=${APP_DIR}
user=www-data
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=${APP_DIR}/storage/logs/reverb.log
EOF

    supervisorctl reread
    supervisorctl update
}

write_cron_schedule() {
    log "Installing cron schedule."

cat >"${CRON_FILE}" <<EOF
* * * * * www-data cd ${APP_DIR} && ${PHP_BIN} artisan schedule:run >> /dev/null 2>&1
EOF

    chmod 0644 "${CRON_FILE}"
}

set_permissions() {
    log "Setting filesystem permissions."

    chown -R www-data:www-data "${APP_DIR}"
    find "${APP_DIR}" -type d -exec chmod 755 {} \;
    chmod -R ug+rwx "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache"
}

restart_services() {
    log "Restarting application services."

    restart_service "${PHP_FPM_SERVICE}" || fail "Could not restart PHP-FPM service ${PHP_FPM_SERVICE}."
    restart_service nginx || fail "Could not restart Nginx."
    restart_service supervisor || fail "Could not restart Supervisor."
    supervisorctl restart "${APP_SLUG}-queue" || true
    supervisorctl restart "${APP_SLUG}-reverb" || true
}

write_credentials_summary() {
    log "Writing installation summary."

    cat >"${CREDENTIALS_FILE}" <<EOF
CRM25 installation completed.

URL: ${APP_URL}
Installed version: ${RELEASE_VERSION}
Application path: ${APP_DIR}

Admin user: ${ADMIN_EMAIL}
Admin password: ${ADMIN_PASSWORD}

MySQL database: ${DB_NAME}
MySQL user: ${DB_USER}
MySQL password: ${DB_PASSWORD}

Redis host: 127.0.0.1
Redis port: 6379
Redis password: ${REDIS_PASSWORD}

Reverb app id: ${REVERB_APP_ID}
Reverb app key: ${REVERB_APP_KEY}
Reverb app secret: ${REVERB_APP_SECRET}

Installer log: ${INSTALL_LOG_FILE}
EOF

    chmod 0600 "${CREDENTIALS_FILE}"
}

print_finish_message() {
    cat <<EOF

CRM25 installation completed.

URL: ${APP_URL}
Version: ${RELEASE_VERSION}
Admin login: ${ADMIN_EMAIL}
Admin password: ${ADMIN_PASSWORD}

Saved credentials: ${CREDENTIALS_FILE}
Installer log: ${INSTALL_LOG_FILE}
EOF
}

main() {
    setup_output_style
    require_root
    enable_install_logging

    log "Starting CRM25 installer."
    log "Full output is being written to ${INSTALL_LOG_FILE}."

    run_stage "Checking root privileges" require_root
    run_stage "Validating apt-based Linux environment" require_apt
    run_stage "Collecting domain and target URL" prompt_for_host
    log_target_summary
    run_stage "Installing system packages" install_system_packages
    run_stage "Detecting runtime services" detect_runtime_services
    log_runtime_versions
    run_stage "Starting infrastructure services" enable_core_services
    run_stage "Fetching latest CRM25 release" fetch_latest_release
    run_stage "Preparing application directory" prepare_application_files
    run_stage "Configuring MySQL database" configure_mysql
    run_stage "Securing Redis" configure_redis
    run_stage "Configuring Nginx" configure_nginx
    run_stage "Configuring TLS when available" obtain_tls_certificate_if_needed
    run_stage "Writing application environment" write_env_file
    run_stage "Installing PHP and frontend dependencies" install_php_and_frontend_dependencies
    run_stage "Provisioning CRM data and admin account" provision_application
    run_stage "Setting filesystem permissions" set_permissions
    run_stage "Configuring Supervisor workers" write_supervisor_configs
    run_stage "Installing cron schedule" write_cron_schedule
    run_stage "Restarting application services" restart_services
    run_stage "Saving credentials and log locations" write_credentials_summary
    print_finish_message
}

main "$@"
