#!/usr/bin/env bash
# One-use Task24 deployment: enable only dualcoachtest's explicit draft-delivery gate.
# No Telegram callback, provider request, draft, customer, registry, or default-profile
# write occurs here. The sole profile write is adaptive_nutrition.delivery_enabled false
# -> true, followed by one controlled user-service restart.
set -euo pipefail

readonly PROFILE=/home/cube/.hermes/profiles/dualcoachtest
readonly PROFILE_NAME=dualcoachtest
readonly CONFIG="$PROFILE/config.yaml"
readonly PROFILES=/home/cube/.hermes/profiles
readonly SERVICE=hermes-gateway-dualcoachtest.service
readonly EVIDENCE_DIR='/home/cube/projects/richard/traning coach/.omo/evidence'
readonly PINS="$EVIDENCE_DIR/dualcoach-task-24-live-deployment-pins.json"
readonly APPROVAL_PHRASE=TASK24_SYNTHETIC_DELIVERY_DEPLOYMENT_APPROVED

usage() {
  cat <<'EOF'
Usage:
  dualcoach-task-24-enable-synthetic-delivery.sh --seal
  TASK24_DEPLOYMENT_APPROVAL=TASK24_SYNTHETIC_DELIVERY_DEPLOYMENT_APPROVED \
  TASK24_DEPLOYMENT_SEAL="$(dualcoach-task-24-enable-synthetic-delivery.sh --seal)" \
  dualcoach-task-24-enable-synthetic-delivery.sh --apply

--apply is one-use. It refuses a stale state, existing delivery, another enabled
customer, a symlinked target, or a pre-existing rollback-pin artifact.
EOF
}

snapshot() {
  local output="$1"
  PROFILE_PATH="$PROFILE" PROFILES_PATH="$PROFILES" OUTPUT="$output" python3 - <<'PY'
import hashlib
import json
import os
from pathlib import Path
import yaml

profile = Path(os.environ['PROFILE_PATH'])
profiles = Path(os.environ['PROFILES_PATH'])
output = Path(os.environ['OUTPUT'])
owner = profile / 'data' / 'owner-actions'

def sha(path):
    return hashlib.sha256(path.read_bytes()).hexdigest() if path.exists() else None

def load(path, default):
    return json.loads(path.read_text(encoding='utf-8')) if path.exists() else default

def enabled_targets(root):
    registry = root / 'customers' / 'registry.json'
    if not registry.is_file():
        return []
    payload = load(registry, {})
    values = payload.get('customers') if isinstance(payload, dict) else []
    return [str(row.get('customer_key', '')) for row in values if isinstance(row, dict) and row.get('enabled') is True]

def delivery_gate(root):
    path = root / 'config.yaml'
    if not path.is_file():
        return None
    config = yaml.safe_load(path.read_text(encoding='utf-8'))
    extra = (((config or {}).get('platforms') or {}).get('telegram') or {}).get('extra') or {}
    adaptive = extra.get('adaptive_nutrition') if isinstance(extra, dict) else None
    return adaptive.get('delivery_enabled') if isinstance(adaptive, dict) else None

all_profiles = []
for candidate in sorted(path for path in profiles.iterdir() if path.is_dir() and not path.is_symlink()):
    all_profiles.append({
        'profile': candidate.name,
        'config_sha256': sha(candidate / 'config.yaml'),
        'delivery_enabled': delivery_gate(candidate),
        'enabled_customer_keys': enabled_targets(candidate),
    })

drafts = load(owner / 'drafts.json', {})
generations = load(owner / 'draft-generations.json', {})
cards = load(owner / 'draft-generation-cards.json', {})
deliveries = load(owner / 'draft-deliveries.json', {})
gateway = load(profile / 'gateway_state.json', {})
platforms = gateway.get('platforms') if isinstance(gateway, dict) else {}
telegram = platforms.get('telegram') if isinstance(platforms, dict) else {}
base = '3f44a18ea620d963'
history = generations.get(base, [])
latest = history[-1] if history else {}
card = cards.get(base, {})
state = {
    'schema': 'task24-synthetic-delivery-deployment-v1',
    'profile': 'dualcoachtest',
    'config_sha256': sha(profile / 'config.yaml'),
    'registry_sha256': sha(profile / 'customers' / 'registry.json'),
    'requests_sha256': sha(owner / 'draft-requests.json'),
    'generations_sha256': sha(owner / 'draft-generations.json'),
    'drafts_sha256': sha(owner / 'drafts.json'),
    'cards_sha256': sha(owner / 'draft-generation-cards.json'),
    'deliveries_sha256': sha(owner / 'draft-deliveries.json'),
    'delivery_count': len(deliveries),
    'delivery_statuses': sorted(str(row.get('status')) for row in deliveries.values() if isinstance(row, dict)),
    'gateway_state': gateway.get('gateway_state') if isinstance(gateway, dict) else None,
    'telegram_connection_state': telegram.get('state') if isinstance(telegram, dict) else None,
    'card_token_digest': hashlib.sha256('\n'.join(sorted(cards)).encode('utf-8')).hexdigest(),
    'all_profiles': all_profiles,
    'current_card': {
        'token': base,
        'draft_status': drafts.get(base, {}).get('status') if isinstance(drafts.get(base), dict) else None,
        'generation_state': latest.get('state') if isinstance(latest, dict) else None,
        'generation': latest.get('generation') if isinstance(latest, dict) else None,
        'generation_record_digest': latest.get('record_digest') if isinstance(latest, dict) else None,
        'generation_provider_receipt_present': isinstance(latest.get('generation_provider_receipt'), str) if isinstance(latest, dict) else False,
        'card_message_id': card.get('message_id') if isinstance(card, dict) else None,
        'card_state': card.get('state') if isinstance(card, dict) else None,
        'card_count': len(cards),
    },
}
output.write_text(json.dumps(state, sort_keys=True, indent=2), encoding='utf-8')
output.chmod(0o600)
print(hashlib.sha256(output.read_bytes()).hexdigest())
PY
}

assert_preconditions() {
  local state="$1"
  python3 - "$state" <<'PY'
import json
import sys
state = json.loads(open(sys.argv[1], encoding='utf-8').read())
def block(message): raise SystemExit('BLOCK: ' + message)
if state['delivery_count'] != 0 or state['deliveries_sha256'] is not None:
    block('delivery ledger is not absent and empty')
if state['current_card'] != {
    'token': '3f44a18ea620d963',
    'draft_status': 'created',
    'generation_state': 'draft_created',
    'generation': 3,
    'generation_record_digest': '42f899e76b603c3f2342be3647ccfa032134c79b67094396564eee6fe4ec429c',
    'generation_provider_receipt_present': True,
    'card_message_id': '153',
    'card_state': 'published',
    'card_count': 8,
}:
    block('current Task23 card/generation projection is not the sealed baseline')
if state['config_sha256'] != 'f93106b16643227e2ef9dec67a5bbd497e1d353e779da62287898a087071af87':
    block('dualcoachtest config digest is not the sealed pre-deployment baseline')
if state['registry_sha256'] != 'f8949a9e158f5c72d49e379d2e9a341c5dc0d126375b1a7522cb69ee74f116f4':
    block('dualcoachtest registry digest is not the sealed pre-deployment baseline')
if state['generations_sha256'] != '5663387e86233e898801a342f11401d265dd4b0ef61fe769945d08b3467fe391':
    block('generation ledger digest is not the sealed pre-deployment baseline')
allowed = [{'profile': 'dualcoachtest', 'config_sha256': state['config_sha256'], 'delivery_enabled': False, 'enabled_customer_keys': ['task22_dm_rehearsal']}]
with_enabled = [row for row in state['all_profiles'] if row['enabled_customer_keys']]
if with_enabled != allowed:
    block('an enabled delivery target exists outside the sole synthetic customer')
if len(state['all_profiles']) != 2:
    block('profile set differs from the sealed scope')
expected_profiles = [
    {'profile': 'dualcoachtest', 'config_sha256': state['config_sha256'], 'delivery_enabled': False, 'enabled_customer_keys': ['task22_dm_rehearsal']},
    {'profile': 'physique-coach', 'config_sha256': state['all_profiles'][1]['config_sha256'], 'delivery_enabled': False, 'enabled_customer_keys': []},
]
if state['all_profiles'] != expected_profiles:
    block('another profile delivery gate or enabled customer differs from the sealed scope')
PY
}

seal() {
  local dir state
  dir="$(mktemp -d /home/cube/.cache/senpi-task23-tmp/task24-deploy-seal.XXXXXX)"
  state="$dir/state.json"
  snapshot "$state" >/dev/null
  assert_preconditions "$state"
  python3 - "$state" <<'PY'
import hashlib
import json
import sys
state = json.loads(open(sys.argv[1], encoding='utf-8').read())
bound = {
    'schema': 'task24-synthetic-delivery-deployment-seal-v1',
    'config_sha256': state['config_sha256'],
    'registry_sha256': state['registry_sha256'],
    'requests_sha256': state['requests_sha256'],
    'generations_sha256': state['generations_sha256'],
    'drafts_sha256': state['drafts_sha256'],
    'cards_sha256': state['cards_sha256'],
    'deliveries_sha256': state['deliveries_sha256'],
    'all_profiles': state['all_profiles'],
    'current_card': state['current_card'],
}
print(hashlib.sha256(json.dumps(bound, sort_keys=True, separators=(',', ':')).encode()).hexdigest())
PY
  rm -rf "$dir"
}

apply() {
  local run before after expected actual
  expected="$(seal)"
  if [[ "${TASK24_DEPLOYMENT_APPROVAL:-}" != "$APPROVAL_PHRASE" ]]; then
    echo 'BLOCK: missing exact Task24 synthetic-delivery deployment approval.' >&2
    exit 64
  fi
  if [[ "${TASK24_DEPLOYMENT_SEAL:-}" != "$expected" ]]; then
    echo 'BLOCK: supplied deployment seal does not match the current sealed baseline.' >&2
    exit 64
  fi
  if [[ -e "$PINS" ]]; then
    echo "BLOCK: rollback pins already exist at $PINS; deployment is one-use." >&2
    exit 64
  fi
  if [[ -L "$CONFIG" || ! -f "$CONFIG" ]]; then
    echo 'BLOCK: target config is not a regular file.' >&2
    exit 64
  fi

  umask 077
  run="$(mktemp -d /home/cube/.cache/senpi-task23-tmp/task24-deploy.XXXXXX)"
  before="$run/before.json"
  after="$run/after.json"
  snapshot "$before" >/dev/null
  assert_preconditions "$before"

  BEFORE="$before" CONFIG_PATH="$CONFIG" PINS_PATH="$PINS" python3 - <<'PY'
import hashlib
import json
import os
import stat
import tempfile
from pathlib import Path

before = json.loads(Path(os.environ['BEFORE']).read_text(encoding='utf-8'))
config = Path(os.environ['CONFIG_PATH'])
pins = Path(os.environ['PINS_PATH'])
raw = config.read_bytes()
needle = b'      adaptive_nutrition:\n        enabled: true\n        delivery_enabled: false\n'
replacement = b'      adaptive_nutrition:\n        enabled: true\n        delivery_enabled: true\n'
if raw.count(needle) != 1:
    raise SystemExit('BLOCK: exact scoped delivery_enabled:false field is absent or ambiguous')
updated = raw.replace(needle, replacement, 1)
if len(updated) != len(raw) - 1:
    raise SystemExit('BLOCK: scoped config patch changed an unexpected byte count')
post_hash = hashlib.sha256(updated).hexdigest()
pin = {
    'schema': 'task24-delivery-gate-rollback-pins-v1',
    'scope': 'dualcoachtest adaptive_nutrition.delivery_enabled only',
    'approval_seal': os.environ.get('TASK24_DEPLOYMENT_SEAL'),
    'pre_config_sha256': hashlib.sha256(raw).hexdigest(),
    'post_config_sha256': post_hash,
    'rollback_precondition_sha256': post_hash,
    'rollback_exact_replacement': 'adaptive_nutrition.delivery_enabled true -> false',
    'profile_ledger_pins': {
        key: before[key]
        for key in ('registry_sha256', 'requests_sha256', 'generations_sha256', 'drafts_sha256', 'cards_sha256', 'deliveries_sha256')
    },
    'current_card': before['current_card'],
    'allowed_enabled_target': 'task22_dm_rehearsal',
}
fd, temp_name = tempfile.mkstemp(prefix='.task24-pins.', dir=str(pins.parent))
try:
    os.fchmod(fd, 0o600)
    with os.fdopen(fd, 'w', encoding='utf-8') as handle:
        json.dump(pin, handle, sort_keys=True, indent=2)
        handle.write('\n')
        handle.flush()
        os.fsync(handle.fileno())
    os.replace(temp_name, pins)
    dir_fd = os.open(pins.parent, os.O_DIRECTORY)
    try: os.fsync(dir_fd)
    finally: os.close(dir_fd)
except BaseException:
    try: os.unlink(temp_name)
    except FileNotFoundError: pass
    raise

mode = stat.S_IMODE(config.stat().st_mode)
fd, temp_name = tempfile.mkstemp(prefix='.task24-delivery.', dir=str(config.parent))
try:
    os.fchmod(fd, mode)
    with os.fdopen(fd, 'wb') as handle:
        handle.write(updated)
        handle.flush()
        os.fsync(handle.fileno())
    os.replace(temp_name, config)
    dir_fd = os.open(config.parent, os.O_DIRECTORY)
    try: os.fsync(dir_fd)
    finally: os.close(dir_fd)
except BaseException:
    try: os.unlink(temp_name)
    except FileNotFoundError: pass
    raise
print(post_hash)
PY

  # The only service mutation in this deployment. No Telegram action is issued here.
  systemctl --user restart "$SERVICE"
  systemctl --user is-active --quiet "$SERVICE" || { echo 'BLOCK: gateway is not active after the single controlled restart.' >&2; exit 1; }

  snapshot "$after" >/dev/null
  BEFORE="$before" AFTER="$after" PINS_PATH="$PINS" python3 - <<'PY'
import json
import sys
from pathlib import Path
before=json.loads(Path(__import__('os').environ['BEFORE']).read_text(encoding='utf-8'))
after=json.loads(Path(__import__('os').environ['AFTER']).read_text(encoding='utf-8'))
pins=json.loads(Path(__import__('os').environ['PINS_PATH']).read_text(encoding='utf-8'))
def block(message): raise SystemExit('BLOCK: '+message)
if after['config_sha256'] != pins['post_config_sha256'] or after['config_sha256'] == before['config_sha256']:
    block('post-restart config digest does not match the sealed one-field deployment')
if after['all_profiles'][0]['delivery_enabled'] is not True or after['all_profiles'][1] != before['all_profiles'][1]:
    block('delivery gate scope extended beyond dualcoachtest')
for key in ('registry_sha256','requests_sha256','generations_sha256','drafts_sha256','deliveries_sha256','delivery_count','delivery_statuses','current_card','card_token_digest'):
    if after[key] != before[key]:
        block(f'unexpected profile state changed during deploy/restart: {key}')
if after['gateway_state'] != 'running' or after['telegram_connection_state'] != 'connected':
    block('gateway did not return to running Telegram-connected state')
print('DEPLOYMENT: PASS')
print('rollback_pins='+str(Path(__import__('os').environ['PINS_PATH'])))
PY
}

case "${1:-}" in
  --seal) seal ;;
  --apply) apply ;;
  *) usage; exit 64 ;;
esac
