#!/usr/bin/env bash
set -Eeuo pipefail

RUZIN_IMAGE="${RUZIN_IMAGE:-docker.puyastudio.ir/ruzin:v1.0.0}"
INSTALL_DIR="${RUZIN_INSTALL_DIR:-/opt/ruzin}"
DEFAULT_PORT="${RUZIN_PORT:-5227}"
DEFAULT_BIND="${RUZIN_BIND_ADDRESS:-127.0.0.1}"

say() { printf '\n==> %s\n' "$*"; }
die() { printf '\nERROR: %s\n' "$*" >&2; exit 1; }

if [[ ! -r /etc/os-release ]]; then
  die "This installer requires Ubuntu."
fi

# shellcheck disable=SC1091
source /etc/os-release
[[ "${ID:-}" == "ubuntu" ]] || die "This installer supports Ubuntu only (detected: ${ID:-unknown})."

arch="$(dpkg --print-architecture 2>/dev/null || true)"
[[ "$arch" == "amd64" ]] || die "Ruzin v1.0.0 container deployment currently supports Ubuntu amd64 only (detected: $arch)."

if [[ "$(id -u)" -eq 0 ]]; then
  SUDO=()
else
  command -v sudo >/dev/null 2>&1 || die "sudo is required."
  sudo -v
  SUDO=(sudo)
fi

docker_cmd() { "${SUDO[@]}" docker "$@"; }
compose() { "${SUDO[@]}" docker compose --project-directory "$INSTALL_DIR" -f "$INSTALL_DIR/compose.yaml" "$@"; }

install_docker() {
  if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
    return
  fi

  say "Installing Docker Engine and Docker Compose plugin"

  "${SUDO[@]}" apt-get update
  "${SUDO[@]}" apt-get install -y ca-certificates curl gnupg openssl

  "${SUDO[@]}" install -m 0755 -d /etc/apt/keyrings
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg |
    "${SUDO[@]}" gpg --dearmor -o /etc/apt/keyrings/docker.gpg
  "${SUDO[@]}" chmod a+r /etc/apt/keyrings/docker.gpg

  . /etc/os-release
  echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" |
    "${SUDO[@]}" tee /etc/apt/sources.list.d/docker.list >/dev/null

  "${SUDO[@]}" apt-get update
  "${SUDO[@]}" apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  "${SUDO[@]}" systemctl enable --now docker
}

read_secret_twice() {
  local prompt="$1"
  local first second
  while true; do
    read -r -s -p "$prompt: " first
    printf '\n'
    read -r -s -p "Confirm: " second
    printf '\n'

    [[ "$first" == "$second" ]] || { echo "Values do not match." >&2; continue; }
    [[ ${#first} -ge 12 ]] || { echo "Use at least 12 characters." >&2; continue; }
    [[ "$first" =~ [A-Z] ]] || { echo "Include an uppercase letter." >&2; continue; }
    [[ "$first" =~ [a-z] ]] || { echo "Include a lowercase letter." >&2; continue; }
    [[ "$first" =~ [0-9] ]] || { echo "Include a digit." >&2; continue; }
    [[ "$first" =~ [^A-Za-z0-9] ]] || { echo "Include a symbol." >&2; continue; }
    printf '%s' "$first"
    return
  done
}

validate_single_line() {
  local name="$1" value="$2"
  [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] ||
    die "$name must be a single line."
}

write_runtime_env() {
  local bootstrap_enabled="$1"
  umask 077
  {
    printf 'ASPNETCORE_ENVIRONMENT=Production\n'
    printf 'ASPNETCORE_URLS=http://0.0.0.0:8080\n'
    printf 'ConnectionStrings__Ruzin=%s\n' "$CONNECTION_STRING"
    printf 'Authentication__Local__Enabled=true\n'
    printf 'Authentication__ExternalSso__Enabled=false\n'
    printf 'Bootstrap__InitialSetup__Enabled=%s\n' "$bootstrap_enabled"

    if [[ "$bootstrap_enabled" == "true" ]]; then
      printf 'Bootstrap__InitialSetup__AdministratorUsername=%s\n' "$ADMIN_USERNAME"
      printf 'Bootstrap__InitialSetup__AdministratorPassword=%s\n' "$ADMIN_PASSWORD"
      printf 'Bootstrap__InitialSetup__AdministratorNationalCode=%s\n' "$ADMIN_NATIONAL_CODE"
      printf 'Bootstrap__InitialSetup__AdministratorFirstName=%s\n' "$ADMIN_FIRST_NAME"
      printf 'Bootstrap__InitialSetup__AdministratorLastName=%s\n' "$ADMIN_LAST_NAME"
      printf 'Bootstrap__InitialSetup__RootOrganizationTitle=%s\n' "$ROOT_ORGANIZATION"
    fi
  } | "${SUDO[@]}" tee "$INSTALL_DIR/ruzin.env" >/dev/null
  "${SUDO[@]}" chmod 600 "$INSTALL_DIR/ruzin.env"
}

wait_for_sql() {
  local container_id status
  container_id="$(compose ps -q sql)"
  [[ -n "$container_id" ]] || die "SQL Server container did not start."

  say "Waiting for SQL Server"
  for _ in $(seq 1 90); do
    status="$(docker_cmd inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container_id" 2>/dev/null || true)"
    if [[ "$status" == "healthy" ]]; then
      return
    fi
    if [[ "$status" == "unhealthy" ]]; then
      compose logs --tail=100 sql >&2 || true
      die "SQL Server health check failed."
    fi
    sleep 2
  done

  compose logs --tail=100 sql >&2 || true
  die "Timed out waiting for SQL Server."
}

wait_for_ruzin() {
  say "Waiting for Ruzin readiness"
  for _ in $(seq 1 90); do
    if curl -fsS --max-time 3 "http://${HEALTH_HOST}:${HOST_PORT}/health/ready" >/dev/null 2>&1; then
      return
    fi
    sleep 2
  done

  compose logs --tail=150 ruzin >&2 || true
  die "Ruzin did not become ready."
}

install_docker

missing_tools=()
for tool in curl openssl; do
  command -v "$tool" >/dev/null 2>&1 || missing_tools+=("$tool")
done
if (( ${#missing_tools[@]} > 0 )); then
  say "Installing required host tools: ${missing_tools[*]}"
  "${SUDO[@]}" apt-get update
  "${SUDO[@]}" apt-get install -y ca-certificates "${missing_tools[@]}"
fi

say "Preparing installation directory: $INSTALL_DIR"
"${SUDO[@]}" install -d -m 0700 "$INSTALL_DIR"

printf '\nRuzin image: %s\n' "$RUZIN_IMAGE"
if ! docker_cmd pull "$RUZIN_IMAGE"; then
  printf '\nThe registry requires authentication or the pull failed.\n'
  read -r -p "Registry username: " REGISTRY_USERNAME
  read -r -s -p "Registry password: " REGISTRY_PASSWORD
  printf '\n'
  printf '%s' "$REGISTRY_PASSWORD" |
    "${SUDO[@]}" docker login docker.puyastudio.ir --username "$REGISTRY_USERNAME" --password-stdin
  unset REGISTRY_PASSWORD
  docker_cmd pull "$RUZIN_IMAGE"
fi

read -r -p "Host port for Ruzin [$DEFAULT_PORT]: " HOST_PORT
HOST_PORT="${HOST_PORT:-$DEFAULT_PORT}"
[[ "$HOST_PORT" =~ ^[0-9]+$ ]] && (( HOST_PORT >= 1 && HOST_PORT <= 65535 )) ||
  die "Invalid TCP port."

printf 'Bind address [%s] (supported: 127.0.0.1 or 0.0.0.0): ' "$DEFAULT_BIND"
read -r BIND_ADDRESS
BIND_ADDRESS="${BIND_ADDRESS:-$DEFAULT_BIND}"
validate_single_line "Bind address" "$BIND_ADDRESS"
[[ "$BIND_ADDRESS" == "127.0.0.1" || "$BIND_ADDRESS" == "0.0.0.0" ]] ||
  die "Use 127.0.0.1 for a same-host HTTPS reverse proxy or 0.0.0.0 to bind all IPv4 interfaces."
HEALTH_HOST=127.0.0.1

printf '\nIf you already have SQL Server, paste its Ruzin connection string.\n'
printf 'The connection is opened from inside the Ruzin container. For SQL Server on this same Ubuntu host, use host.docker.internal rather than localhost.\n'
printf 'Leave it blank and this installer will provision SQL Server 2022 in Docker.\n'
read -r -s -p "Existing SQL Server connection string (hidden): " CONNECTION_STRING
printf '\n'
validate_single_line "Connection string" "$CONNECTION_STRING"

LOCAL_SQL=false
SQL_PID=""
SA_PASSWORD=""
APP_SQL_PASSWORD=""

if [[ -z "$CONNECTION_STRING" ]]; then
  LOCAL_SQL=true

  printf '\nMicrosoft SQL Server requires acceptance of its license terms.\n'
  read -r -p "Provision SQL Server 2022 and accept the Microsoft SQL Server EULA? [y/N]: " ACCEPT_SQL_EULA
  [[ "$ACCEPT_SQL_EULA" =~ ^[Yy]$ ]] || die "SQL Server EULA was not accepted; installation stopped."

  printf '\nSQL Server edition/PID examples: Express, Standard, Enterprise, or a product key.\n'
  printf 'Express is free but has SQL Server Express limits. Developer is not licensed for production use.\n'
  read -r -p "SQL Server PID [Express]: " SQL_PID
  SQL_PID="${SQL_PID:-Express}"
  validate_single_line "SQL Server PID" "$SQL_PID"

  if [[ "${SQL_PID,,}" == "developer" ]]; then
    read -r -p "Developer edition is for non-production use only. Continue? [y/N]: " DEV_CONFIRM
    [[ "$DEV_CONFIRM" =~ ^[Yy]$ ]] || die "Choose a production-appropriate SQL Server edition."
  fi

  SA_PASSWORD="$(read_secret_twice "SQL Server sa password")"
  APP_SQL_PASSWORD="$(openssl rand -hex 24)Aa9!"

  CONNECTION_STRING="Server=sql,1433;Database=Ruzin;User Id=ruzin_app;Password=${APP_SQL_PASSWORD};Encrypt=True;TrustServerCertificate=True"
fi

BOOTSTRAP_DEFAULT="n"
[[ "$LOCAL_SQL" == "true" ]] && BOOTSTRAP_DEFAULT="y"
if [[ "$BOOTSTRAP_DEFAULT" == "y" ]]; then
  BOOTSTRAP_PROMPT="[Y/n]"
else
  BOOTSTRAP_PROMPT="[y/N]"
fi
read -r -p "Create the initial Ruzin administrator and root organization? $BOOTSTRAP_PROMPT: " BOOTSTRAP_ANSWER
BOOTSTRAP_ANSWER="${BOOTSTRAP_ANSWER:-$BOOTSTRAP_DEFAULT}"

BOOTSTRAP=false
if [[ "$BOOTSTRAP_ANSWER" =~ ^[Yy]$ ]]; then
  BOOTSTRAP=true

  read -r -p "Root organization title: " ROOT_ORGANIZATION
  [[ -n "$ROOT_ORGANIZATION" ]] || die "Root organization title is required."
  validate_single_line "Root organization title" "$ROOT_ORGANIZATION"

  read -r -p "Administrator username [admin]: " ADMIN_USERNAME
  ADMIN_USERNAME="${ADMIN_USERNAME:-admin}"
  validate_single_line "Administrator username" "$ADMIN_USERNAME"

  read -r -p "Administrator national code: " ADMIN_NATIONAL_CODE
  [[ -n "$ADMIN_NATIONAL_CODE" ]] || die "Administrator national code is required."
  validate_single_line "Administrator national code" "$ADMIN_NATIONAL_CODE"

  read -r -p "Administrator first name [System]: " ADMIN_FIRST_NAME
  ADMIN_FIRST_NAME="${ADMIN_FIRST_NAME:-System}"
  validate_single_line "Administrator first name" "$ADMIN_FIRST_NAME"

  read -r -p "Administrator last name [Administrator]: " ADMIN_LAST_NAME
  ADMIN_LAST_NAME="${ADMIN_LAST_NAME:-Administrator}"
  validate_single_line "Administrator last name" "$ADMIN_LAST_NAME"

  ADMIN_PASSWORD="$(read_secret_twice "Administrator password")"
else
  ROOT_ORGANIZATION=""
  ADMIN_USERNAME=""
  ADMIN_NATIONAL_CODE=""
  ADMIN_FIRST_NAME=""
  ADMIN_LAST_NAME=""
  ADMIN_PASSWORD=""
fi

say "Writing Docker Compose configuration"

if [[ "$LOCAL_SQL" == "true" ]]; then
  umask 077
  {
    printf 'ACCEPT_EULA=Y\n'
    printf 'MSSQL_PID=%s\n' "$SQL_PID"
    printf 'MSSQL_SA_PASSWORD=%s\n' "$SA_PASSWORD"
  } | "${SUDO[@]}" tee "$INSTALL_DIR/sql.env" >/dev/null
  "${SUDO[@]}" chmod 600 "$INSTALL_DIR/sql.env"

  cat <<EOF_COMPOSE | "${SUDO[@]}" tee "$INSTALL_DIR/compose.yaml" >/dev/null
name: ruzin
services:
  sql:
    image: mcr.microsoft.com/mssql/server:2022-latest
    restart: unless-stopped
    env_file:
      - path: ./sql.env
        format: raw
    volumes:
      - sql-data:/var/opt/mssql
    healthcheck:
      test: ["CMD-SHELL", "SQLCMD=/opt/mssql-tools18/bin/sqlcmd; [ -x \"\$\$SQLCMD\" ] || SQLCMD=/opt/mssql-tools/bin/sqlcmd; \"\$\$SQLCMD\" -S localhost -U sa -P \"\$\$MSSQL_SA_PASSWORD\" -C -Q 'SELECT 1' >/dev/null 2>&1 || exit 1"]
      interval: 5s
      timeout: 5s
      retries: 30
      start_period: 20s

  ruzin:
    image: ${RUZIN_IMAGE}
    restart: unless-stopped
    env_file:
      - path: ./ruzin.env
        format: raw
    ports:
      - "${BIND_ADDRESS}:${HOST_PORT}:8080"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - data-protection:/home/app/.aspnet/DataProtection-Keys
    depends_on:
      sql:
        condition: service_healthy
    stop_grace_period: 30s

volumes:
  sql-data:
  data-protection:
EOF_COMPOSE
else
  "${SUDO[@]}" rm -f "$INSTALL_DIR/sql.env"

  cat <<EOF_COMPOSE | "${SUDO[@]}" tee "$INSTALL_DIR/compose.yaml" >/dev/null
name: ruzin
services:
  ruzin:
    image: ${RUZIN_IMAGE}
    restart: unless-stopped
    env_file:
      - path: ./ruzin.env
        format: raw
    ports:
      - "${BIND_ADDRESS}:${HOST_PORT}:8080"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - data-protection:/home/app/.aspnet/DataProtection-Keys
    stop_grace_period: 30s

volumes:
  data-protection:
EOF_COMPOSE
fi

"${SUDO[@]}" chmod 600 "$INSTALL_DIR/compose.yaml"

write_runtime_env "$([[ "$BOOTSTRAP" == "true" ]] && echo true || echo false)"

compose config >/dev/null

if [[ "$LOCAL_SQL" == "true" ]]; then
  say "Starting SQL Server"
  compose up -d sql
  wait_for_sql

  say "Creating the Ruzin database and dedicated SQL login"
  sql_setup="$(cat <<EOF_SQL
SET NOCOUNT ON;
IF DB_ID(N'Ruzin') IS NULL
BEGIN
    CREATE DATABASE [Ruzin];
END;
IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = N'ruzin_app')
BEGIN
    CREATE LOGIN [ruzin_app] WITH PASSWORD = N'${APP_SQL_PASSWORD}', CHECK_POLICY = ON;
END;
USE [Ruzin];
IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = N'ruzin_app')
BEGIN
    CREATE USER [ruzin_app] FOR LOGIN [ruzin_app];
END;
IF IS_ROLEMEMBER(N'db_owner', N'ruzin_app') <> 1
BEGIN
    ALTER ROLE [db_owner] ADD MEMBER [ruzin_app];
END;
EOF_SQL
)"
  printf '%s\n' "$sql_setup" |
    compose exec -T sql bash -lc 'SQLCMD=/opt/mssql-tools18/bin/sqlcmd; [ -x "$SQLCMD" ] || SQLCMD=/opt/mssql-tools/bin/sqlcmd; SQLCMDPASSWORD="$MSSQL_SA_PASSWORD" "$SQLCMD" -S localhost -U sa -C -b'
  unset sql_setup APP_SQL_PASSWORD SA_PASSWORD
fi

say "Applying Ruzin database migrations explicitly"
compose run --rm --no-deps ruzin --migrate

say "Starting Ruzin"
compose up -d ruzin
wait_for_ruzin

if [[ "$BOOTSTRAP" == "true" ]]; then
  say "Initial administrator/root setup completed; removing bootstrap secret from runtime configuration"
  write_runtime_env false
  unset ADMIN_PASSWORD

  compose up -d --force-recreate ruzin
  wait_for_ruzin
fi

say "Installation completed"
printf '\nImage: %s\n' "$RUZIN_IMAGE"
printf 'Local readiness: http://127.0.0.1:%s/health/ready\n' "$HOST_PORT"

if [[ "$BIND_ADDRESS" == "127.0.0.1" || "$BIND_ADDRESS" == "::1" ]]; then
  printf 'Ruzin is bound to loopback. Put nginx/Caddy/another trusted same-host reverse proxy with HTTPS in front of it for remote access.\n'
else
  printf 'Ruzin is exposed directly on %s:%s over HTTP. Configure HTTPS/reverse proxy before sending credentials over an untrusted network.\n' "$BIND_ADDRESS" "$HOST_PORT"
fi

printf '\nUseful commands:\n'
printf '  cd %s && sudo docker compose ps\n' "$INSTALL_DIR"
printf '  cd %s && sudo docker compose logs -f ruzin\n' "$INSTALL_DIR"
printf '  cd %s && sudo docker compose pull && sudo docker compose run --rm --no-deps ruzin --migrate && sudo docker compose up -d\n' "$INSTALL_DIR"