#!/bin/bash
#
# Install a plain panel for one tenant, on any server, linked to Final Target.
#
# What "plain" means here, and why it matters: the code comes from Final Target,
# nothing else does. No seeded pages, no footer links, no settings row copied
# from another site, no backups folder carried across, and no credentials. The
# panel comes up empty and is filled from its own Settings page.
#
# This exists because the alternative, rsyncing a working install onto a new
# domain, put Final Target's content and Final Target's backups onto a client
# site on 20 August 2026 and took a day to unpick.
#
# What it does, in order:
#   1. Creates the PostgreSQL database and role
#   2. Copies the code, excluding venv, media, staticfiles, backups and .env
#   3. Writes a fresh .env with keys generated on this machine
#   4. Builds the virtualenv and runs migrations
#   5. Creates the tenant row with is_master set, and one admin user
#   6. Collects static, writes the systemd unit, starts the service
#
# What it deliberately does NOT do:
#   - copy any .env, key or token from another install
#   - run any seed command
#   - configure Apache or DNS, which stay yours
#   - register with Final Target, which needs the key from the Fleet page
#
# One command, whole site:
#   ./ft-tenant-install.sh --yes --apache \
#       --name "MOSTAFAEI" --domain mostafaei.com --user mostafaei --port 8002 \
#       --plan signature --modules homepage,portfolio,insights,services,chat \
#       --admin meghdad@finaltarget.com \
#       --ft-url https://panel.finaltarget.com/api/ftlink/checkin/ --ft-key <key>
#
# Minimum:
#   ./ft-tenant-install.sh --name "MOSTAFAEI" --domain mostafaei.com --user mostafaei --port 8002
#
# Optional:
#   --source  the install to copy code from, default Final Target
#   --python  interpreter to build the venv with, default is the source venv's
#             own, then the newest python3.1x on the box. Must be 3.10 or newer.
#
set -euo pipefail

# The credit line at the foot of every public page this install serves. It is
# written into the new tenant's Settings during install, so it is there from
# the first request and does not depend on anyone filling a form in. Set
# BUILDER_NAME= and BUILDER_URL= in the environment before running this.
BUILDER_NAME="${BUILDER_NAME:-}"
BUILDER_URL="${BUILDER_URL:-}"

# Who hosts this site, shown on the System Info page. Every site on this
# estate is hosted by whoever runs the fleet, so it is set here rather than
# left for someone to remember. Override with HOSTING_PROVIDER= in the
# environment when a site sits somewhere else.
HOSTING_PROVIDER="${HOSTING_PROVIDER:-}"

NAME=""; DOMAIN=""; SYSUSER=""; PORT=""; PANEL=""; SOURCE=""
SLUG=""; PYBIN=""; PLAN="starter"; MODULES=""

# Every tenant install carries one Super Administrator, and it is ours. The
# client gets their own accounts underneath it. Overriding this with --admin
# is possible but should be rare: an install whose top account belongs to the
# client cannot be supported without asking them to log in for us.
ADMIN_EMAIL="meghdad@finaltarget.com"
ASSUME_YES=0; WRITE_APACHE=0; FT_URL=""; FT_HOST=""; FT_KEY=""

while [ $# -gt 0 ]; do
    case "$1" in
        --name)   NAME="$2"; shift 2 ;;
        --domain) DOMAIN="$2"; shift 2 ;;
        --user)   SYSUSER="$2"; shift 2 ;;
        --port)   PORT="$2"; shift 2 ;;
        --panel)  PANEL="$2"; shift 2 ;;
        --slug)   SLUG="$2"; shift 2 ;;
        --source) SOURCE="$2"; shift 2 ;;
        --python) PYBIN="$2"; shift 2 ;;
        --plan)    PLAN="$2"; shift 2 ;;
        --modules) MODULES="$2"; shift 2 ;;
        --admin)   ADMIN_EMAIL="$2"; shift 2 ;;
        --ft-url)  FT_URL="$2"; shift 2 ;;
        --ft-host) FT_HOST="$2"; shift 2 ;;
        --ft-key)  FT_KEY="$2"; shift 2 ;;
        --apache)  WRITE_APACHE=1; shift ;;
        --yes|-y)  ASSUME_YES=1; shift ;;
        *) echo "Unknown option: $1"; exit 1 ;;
    esac
done

if [ -z "$NAME" ] || [ -z "$DOMAIN" ] || [ -z "$SYSUSER" ] || [ -z "$PORT" ]; then
    echo "Required: --name --domain --user --port"
    echo "Example:  $0 --name \"MOSTAFAEI\" --domain mostafaei.com --user mostafaei --port 8002"
    exit 1
fi

# The blank template, not a live site.
#
# This used to copy from /home/finaltarget/public_html/backend, so every new
# install was born from Final Target's own tree. That tree still carries Final
# Target's marketing copy as model defaults, 'Transform Your', 'Online
# Appearance', '1500+ Trusted Clients', which The Panel removed in
# pages/0044_blank_marketing_defaults. HP Global's first page load therefore
# showed Final Target's homepage under HP Global's logo.
#
# /root/the-panel is the blank source and is kept current by the ship script.
SOURCE="${SOURCE:-/root/the-panel}"
PANEL="${PANEL:-panel.${DOMAIN}}"
# Must match what Django's slugify produced when the tenant was created on
# Final Target, because FT_TENANT_SLUG is how the master identifies this
# install at check-in.
#
# The old version deleted every character outside [a-z0-9-], so "HP Global"
# became "hpglobal" while Final Target held "hp-global", and every check-in
# came back 404 "unknown tenant" with nothing to say the two names differed
# by one dash. Spaces and underscores become dashes first, then runs of
# dashes collapse and any leading or trailing dash is trimmed.
SLUG="${SLUG:-$(echo "$NAME" \
    | tr '[:upper:]' '[:lower:]' \
    | tr ' _' '--' \
    | tr -cd '[:alnum:]-' \
    | sed -e 's|--*|-|g' -e 's|^-||' -e 's|-$||')}"
APP_DIR="/home/${SYSUSER}/public_html/backend"
DB_NAME="${SYSUSER}_db"
DB_USER="${SYSUSER}_app"

# ── Interpreter. Checked before anything is created ──────────────────────────
# On AlmaLinux 8 plain python3 is 3.6, which cannot run Django 5. Failing here
# is cheap. Failing after the database and the code are in place is not.
if [ -z "$PYBIN" ]; then
    # The source install already runs on a working interpreter, so prefer that
    # one: it is the version this code is known to work on.
    if [ -x "${SOURCE}/venv/bin/python" ]; then
        PYBIN=$(readlink -f "${SOURCE}/venv/bin/python")
    else
        for candidate in python3.13 python3.12 python3.11 python3.10 python3; do
            if command -v "$candidate" >/dev/null 2>&1; then
                PYBIN=$(command -v "$candidate")
                break
            fi
        done
    fi
fi

if [ -z "$PYBIN" ] || ! "$PYBIN" -c "import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)" 2>/dev/null; then
    FOUND=$([ -n "$PYBIN" ] && "$PYBIN" -V 2>&1 || echo "none found")
    echo "ERROR: need Python 3.10 or newer. Found: ${FOUND}"
    echo "Pass one explicitly, for example:"
    echo "  --python /usr/local/bin/python3.13"
    echo "Or point --source at an install whose venv already runs the right version."
    exit 1
fi

echo "About to install:"
echo "  site      $NAME ($SLUG)"
echo "  domain    $DOMAIN"
echo "  panel     $PANEL"
echo "  code from $SOURCE"
echo "  into      $APP_DIR"
echo "  database  $DB_NAME as $DB_USER"
echo "  gunicorn  127.0.0.1:$PORT"
echo "  python    $PYBIN ($("$PYBIN" -V 2>&1))"
echo "  plan      $PLAN"
echo "  modules   ${MODULES:-whatever the plan grants}"
echo "  admin     ${ADMIN_EMAIL}"
echo "  apache    $([ "$WRITE_APACHE" -eq 1 ] && echo "yes, proxy configs written" || echo "no, yours to do")"
echo "  fleet     $([ -n "$FT_KEY" ] && echo "linked at install" || echo "not linked yet")"
echo
if [ "$ASSUME_YES" -ne 1 ]; then
    read -r -p "Continue? [y/N] " reply
    [ "$reply" = "y" ] || [ "$reply" = "Y" ] || { echo "Stopped."; exit 1; }
fi

if [ -e "$APP_DIR" ]; then
    echo "ERROR: $APP_DIR already exists. Refusing to install over a live site."
    exit 1
fi

# ── 1. Database ──────────────────────────────────────────────────────────────
# The password is generated here and written only to this install's .env. No
# other site's credentials are read, and this one is never printed twice.
DB_PASS=$(openssl rand -base64 24 | tr -d '/+=' | head -c 28)

echo "→ Creating database"
sudo -u postgres psql -v ON_ERROR_STOP=1 <<SQL
CREATE ROLE ${DB_USER} LOGIN PASSWORD '${DB_PASS}';
CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};
SQL

# ── 2. Code ──────────────────────────────────────────────────────────────────
echo "→ Copying code"
mkdir -p "$APP_DIR"
# Leading slashes matter. Unanchored 'media/' also matches apps/media/, which
# silently strips the Media Hub app and leaves an install that will not start.
rsync -a \
    --exclude '/venv/' \
    --exclude '/media/' \
    --exclude '/staticfiles/' \
    --exclude '/backups/' \
    --exclude '/logs/' \
    --exclude '__pycache__/' \
    --exclude '*.pyc' \
    --exclude '/.env' \
    --exclude '/.env.*' \
    --exclude '/db.sqlite3' \
    --exclude '/RELEASE-*.md' \
    "${SOURCE}/" "${APP_DIR}/"
mkdir -p "${APP_DIR}/media" "${APP_DIR}/staticfiles" "${APP_DIR}/.cache"

# ── 3. Environment. Blank sheet, own keys ────────────────────────────────────
echo "→ Writing .env"
SECRET_KEY=$("$PYBIN" -c "import secrets; print(secrets.token_urlsafe(64))")
FIELD_KEY=$("$PYBIN" -c "
try:
    from cryptography.fernet import Fernet
    print(Fernet.generate_key().decode())
except Exception:
    import base64, os
    print(base64.urlsafe_b64encode(os.urandom(32)).decode())
")

cat > "${APP_DIR}/.env" <<ENV
# ${NAME}. This file belongs to this install alone.
# Never copy it to another site, and never paste its contents into a message.

DEBUG=False
SECRET_KEY=${SECRET_KEY}
FIELD_ENCRYPTION_KEY=${FIELD_KEY}
ALLOWED_HOSTS=${DOMAIN},www.${DOMAIN},${PANEL},127.0.0.1

# One variable, because this is the only one the settings actually read.
# DB_NAME and friends were ignored, which sent an install silently to SQLite.
DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@127.0.0.1:5432/${DB_NAME}

# Paths. Without these an install inherits Final Target's and writes its static
# and uploaded files into another site's directories.
STATIC_ROOT=${APP_DIR}/staticfiles
MEDIA_ROOT=${APP_DIR}/media
BACKUP_DIR=/home/${SYSUSER}/backups
CACHE_DIR=${APP_DIR}/.cache

# Mail. Blank on purpose. Fill in from this panel's Settings page, or here.
EMAIL_HOST=
EMAIL_PORT=465
EMAIL_USE_SSL=True
EMAIL_USE_TLS=False
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
DEFAULT_FROM_EMAIL=${NAME} <noreply@${DOMAIN}>

# Payments, AI, captcha, messaging. Each site uses its own accounts.
STRIPE_PUBLIC_KEY=
STRIPE_SECRET_KEY=
OPENAI_API_KEY=
RECAPTCHA_SITE_KEY=
RECAPTCHA_SECRET_KEY=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=

# Hosting and domain, for the System Info cards. Blank is honest: the card
# says "Not recorded" rather than inventing a plan or an expiry date.
HOSTING_PROVIDER=${HOSTING_PROVIDER}
HOSTING_PACKAGE=
HOSTING_TYPE=
HOSTING_IP=
HOSTING_ENDS_ON=
HOSTING_STARTED_ON=
DOMAIN_REGISTRAR=
DOMAIN_EXPIRES_ON=

# Fleet link. Paste the key from this tenant's Connection page on Final Target.
FT_CONTROL_URL=${FT_URL}
FT_CONTROL_HOST=${FT_HOST}
FT_TENANT_SLUG=${SLUG}
FT_TENANT_KEY=${FT_KEY}

# The systemd unit for this install. The panel refuses to restart anything
# when this is blank, rather than restarting a service belonging to another site.
SERVICE_NAME=${SYSUSER}
ENV
chmod 600 "${APP_DIR}/.env"

# ── 4. Virtualenv and schema ─────────────────────────────────────────────────
echo "→ Building virtualenv"
"$PYBIN" -m venv "${APP_DIR}/venv"
"${APP_DIR}/venv/bin/pip" install -q --upgrade pip
"${APP_DIR}/venv/bin/pip" install -q -r "${APP_DIR}/requirements.txt"

echo "→ Running migrations"
cd "$APP_DIR"
venv/bin/python manage.py migrate --settings=config.settings.production

# ── 5. Clear anything the migrations seeded ──────────────────────────────────
# Two data migrations write Final Target's footer links into whatever database
# runs them. On a database created seconds ago there is nothing else to lose,
# so everything goes and the site starts genuinely empty.
echo "→ Clearing seeded content"
venv/bin/python manage.py shell --settings=config.settings.production <<'PYSEED'
from apps.pages.models import FooterLink
count = FooterLink.objects.count()
FooterLink.objects.all().delete()
print(f'  removed {count} seeded footer link(s)')
PYSEED

# ── 6. The tenant row, and nothing else ──────────────────────────────────────
echo "→ Creating the tenant row"
venv/bin/python manage.py ft_tenant_init \
    --name "$NAME" --domain "$DOMAIN" --panel "$PANEL" --slug "$SLUG" \
    --plan "$PLAN" ${MODULES:+--modules "$MODULES"} \
    --builder-name "$BUILDER_NAME" --builder-url "$BUILDER_URL" \
    --settings=config.settings.production

if [ -n "$ADMIN_EMAIL" ]; then
    echo "→ Creating the system administrator"
    # The password is generated here and written to a root only file. It is
    # never printed to the terminal, where it would sit in scrollback and
    # shell history for anyone with the box.
    ADMIN_PASS=$(openssl rand -base64 21 | tr -d '/+=' | head -c 24)
    ADMIN_FILE="/root/${SYSUSER}-admin.txt"
    ADMIN_EMAIL="$ADMIN_EMAIL" ADMIN_PASS="$ADMIN_PASS" \
    venv/bin/python manage.py shell --settings=config.settings.production <<'PYADMIN'
import os
from django.contrib.auth import get_user_model
User = get_user_model()
email = os.environ['ADMIN_EMAIL']
user, created = User.objects.get_or_create(
    email=email, defaults={'username': email.split('@')[0], 'is_staff': True, 'is_superuser': True}
)
user.is_staff = True
user.is_superuser = True
if hasattr(user, 'role'):
    user.role = 'super_admin'
user.set_password(os.environ['ADMIN_PASS'])
user.save()
print(('Created' if created else 'Updated') + f' system administrator: {email}')
PYADMIN
    umask 077
    # The login name is the username, not the email. User extends
    # AbstractUser, so USERNAME_FIELD is 'username', and the account above is
    # created with the local part of the address. Printing only the email here
    # handed out the one credential that cannot log in.
    ADMIN_USERNAME="${ADMIN_EMAIL%%@*}"
    printf 'Site:     %s\nPanel:    https://%s/auth/login/\nUsername: %s\nEmail:    %s\nPassword: %s\n\nSign in with the username, not the email address.\nChange the password after the first login, then delete this file.\n' \
        "$NAME" "$PANEL" "$ADMIN_USERNAME" "$ADMIN_EMAIL" "$ADMIN_PASS" > "$ADMIN_FILE"
    chmod 600 "$ADMIN_FILE"
    echo "  credentials written to $ADMIN_FILE (root only)"
    unset ADMIN_PASS
else
    echo "→ Create the first admin user"
    venv/bin/python manage.py createsuperuser --settings=config.settings.production
fi

# ── 7. Static, service, ownership ────────────────────────────────────────────
echo "→ Collecting static"
venv/bin/python manage.py collectstatic --noinput -v 0 --settings=config.settings.production

mkdir -p "/home/${SYSUSER}/backups"
chmod 700 "/home/${SYSUSER}/backups"
chown -R "${SYSUSER}:${SYSUSER}" "/home/${SYSUSER}/public_html" "/home/${SYSUSER}/backups"

echo "→ Writing the service"
cat > "/etc/systemd/system/${SYSUSER}.service" <<UNIT
[Unit]
Description=${NAME} panel and site
After=network.target postgresql.service

[Service]
User=${SYSUSER}
Group=${SYSUSER}
WorkingDirectory=${APP_DIR}
Environment="DJANGO_SETTINGS_MODULE=config.settings.production"
ExecStart=${APP_DIR}/venv/bin/gunicorn config.wsgi:application \\
    --bind 127.0.0.1:${PORT} --workers 3 --timeout 120
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
UNIT

if [ "$WRITE_APACHE" -eq 1 ]; then
    echo "→ Writing Apache proxy configs"
    for host in "$DOMAIN" "www.${DOMAIN}" "$PANEL"; do
        for kind in std ssl; do
            conf_dir="/etc/apache2/conf.d/userdata/${kind}/2_4/${SYSUSER}/${host}"
            mkdir -p "$conf_dir"
            {
                # Everything goes to gunicorn, /media/ included. An Apache
                # Alias for it was tried on 24 Aug 2026 and lost to the
                # cPanel handler chain: Syntax OK, still 404, request
                # handed to PHP-FPM. Django serves media through the route
                # in config/urls.py instead, and excluding /media/ here
                # would stop that route ever being reached.
                # Without this Apache replaces the Host header with the
                # backend address, so Django sees 127.0.0.1. The panel
                # subdomain middleware matches the request host against
                # cms_subdomain to mount the CMS at /, so a lost Host
                # header means panel.example.com serves the public site
                # instead of the panel. It also made the System Info SSL
                # card report a certificate for 127.0.0.1.
                echo "ProxyPreserveHost On"
                echo "ProxyPass / http://127.0.0.1:${PORT}/"
                echo "ProxyPassReverse / http://127.0.0.1:${PORT}/"
                # Without this Django cannot tell the request was HTTPS, so
                # SECURE_SSL_REDIRECT sends it round in a loop forever.
                [ "$kind" = "ssl" ] && echo 'RequestHeader set X-Forwarded-Proto "https"'
            } > "${conf_dir}/proxy.conf"
        done
    done
    /scripts/ensure_vhost_includes --user="$SYSUSER" 2>/dev/null || \
        echo "  ensure_vhost_includes did not run, do it by hand"
fi

systemctl daemon-reload
systemctl enable --now "${SYSUSER}"
sleep 3
systemctl is-active "${SYSUSER}"

echo
echo "── Installed ───────────────────────────────────────────────────────────"
echo "${NAME} is running on 127.0.0.1:${PORT}."
echo

if [ "$WRITE_APACHE" -eq 1 ]; then
    echo "  Apache   proxy configs written. DNS and the SSL certificate are yours."
else
    echo "  Apache   NOT configured. Point ${DOMAIN}, www.${DOMAIN} and ${PANEL}"
    echo "           at 127.0.0.1:${PORT}, and put this in the SSL config or the"
    echo "           panel will redirect in a loop:"
    echo "               RequestHeader set X-Forwarded-Proto \"https\""
fi

if [ -n "$FT_KEY" ]; then
    echo "  Fleet    linked. Adding the check-in schedule:"
    ( crontab -l 2>/dev/null | grep -v "ft_checkin.*${SYSUSER}"; \
      echo "*/15 * * * * cd ${APP_DIR} && venv/bin/python manage.py ft_checkin --settings=config.settings.production >> /var/log/${SYSUSER}-checkin.log 2>&1" \
    ) | crontab -
    echo "           every 15 minutes, logging to /var/log/${SYSUSER}-checkin.log"
else
    echo "  Fleet    NOT linked. Take FT_CONTROL_URL, FT_CONTROL_HOST and"
    echo "           FT_TENANT_KEY from the tenant's Connection page, put them in"
    echo "           ${APP_DIR}/.env, then add the check-in to cron."
fi

if [ -n "$ADMIN_EMAIL" ]; then
    echo "  Admin    ${ADMIN_EMAIL%%@*} (username), password in /root/${SYSUSER}-admin.txt"
fi

echo "  Settings mail, payments and branding are blank by design. Fill them in"
echo "           from this panel, using this site's own accounts."
echo
echo "The database password is in ${APP_DIR}/.env and nowhere else."
