#!/usr/bin/env node
/**
 * BrandLoop – Appwrite-Schema-Setup (idempotent)
 *
 * Legt im selbst gehosteten Appwrite (Projekt BrandLoop) die komplette
 * Datenbank-Struktur aus dem DB-Plan an: Datenbank, 15 Tabellen (TablesDB),
 * Spalten, Indizes, 4 Storage-Buckets und das interne Dev-Team.
 *
 * Aufruf:
 *   APPWRITE_API_KEY=... node scripts/setup-appwrite.mjs
 * oder Key als BRANDLOOP_APPWRITE_API_KEY in /home/webklar/apps/.env hinterlegen.
 *
 * Idempotent: bereits existierende Objekte (HTTP 409) werden übersprungen –
 * das Skript kann nach Plan-Erweiterungen jederzeit erneut laufen.
 *
 * Abweichung vom Plan: Fremdschlüssel sind indizierte String-Spalten (size 64)
 * statt Appwrite-Relationship-Spalten. Grund: Relationships sind in Appwrite
 * nicht filter-/indizierbar – der geforderte Top-N-Index
 * (brand, category, status, score DESC) und alle Listen-Queries brauchen aber
 * genau das. Many-to-many (videos.attribute_ids) ist ein String-Array
 * (Query.contains funktioniert darauf).
 */

import { readFileSync } from 'node:fs';

const ENDPOINT = process.env.APPWRITE_ENDPOINT || 'https://appwrite.webklar.com/v1';
const PROJECT = process.env.APPWRITE_PROJECT || '6a5cee34002bb8360c34';
const ENV_FILE = process.env.ENV_FILE || '/home/webklar/apps/.env';
const DB_ID = 'brandloop';

function resolveApiKey() {
  if (process.env.APPWRITE_API_KEY) return process.env.APPWRITE_API_KEY;
  try {
    const env = readFileSync(ENV_FILE, 'utf8');
    const m = env.match(/^BRANDLOOP_APPWRITE_API_KEY=(.+)$/m);
    if (m) return m[1].trim();
  } catch { /* .env nicht lesbar – unten Fehlermeldung */ }
  console.error(`Kein API-Key: APPWRITE_API_KEY setzen oder BRANDLOOP_APPWRITE_API_KEY in ${ENV_FILE} hinterlegen.`);
  process.exit(1);
}
const API_KEY = resolveApiKey();

let created = 0, skipped = 0, warned = 0;

async function api(method, path, body) {
  const res = await fetch(`${ENDPOINT}${path}`, {
    method,
    headers: {
      'X-Appwrite-Project': PROJECT,
      'X-Appwrite-Key': API_KEY,
      'Content-Type': 'application/json',
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const text = await res.text();
  let json;
  try { json = JSON.parse(text); } catch { json = { message: text }; }
  return { status: res.status, json };
}

async function ensure(label, method, path, body) {
  const { status, json } = await api(method, path, body);
  if (status >= 200 && status < 300) { created++; console.log(`  + ${label}`); return json; }
  if (status === 409) { skipped++; console.log(`  = ${label} (existiert)`); return null; }
  throw new Error(`${label}: HTTP ${status} – ${json.message}`);
}

async function tryEnsure(label, method, path, body) {
  try { return await ensure(label, method, path, body); }
  catch (e) { warned++; console.warn(`  ! ${label}: ${e.message}`); return null; }
}

// ---- Spalten-Helfer -------------------------------------------------------
const str = (key, size, opts = {}) => ({ type: 'string', body: { key, size, required: false, ...opts } });
const id = (key, opts = {}) => str(key, 64, opts);           // Referenzen: uuid4 = 36 Zeichen, 64 mit Luft
const md = (key, opts = {}) => str(key, 65535, opts);        // Markdown-/Prompt-Texte
const jsonCol = (key, size = 16384, opts = {}) => str(key, size, opts); // JSON als String
const int = (key, opts = {}) => ({ type: 'integer', body: { key, required: false, ...opts } });
const flt = (key, opts = {}) => ({ type: 'float', body: { key, required: false, ...opts } });
const bool = (key, opts = {}) => ({ type: 'boolean', body: { key, required: false, ...opts } });
const dt = (key, opts = {}) => ({ type: 'datetime', body: { key, required: false, ...opts } });
const enm = (key, elements, opts = {}) => ({ type: 'enum', body: { key, elements, required: false, ...opts } });

const P_KEYS = Array.from({ length: 18 }, (_, i) => `P${i + 1}`);
const INTERNAL = 'team:internal';
const internalOnly = [
  `read("${INTERNAL}")`, `create("${INTERNAL}")`,
  `update("${INTERNAL}")`, `delete("${INTERNAL}")`,
];

// ---- Tabellen-Definitionen (Spalten + Indizes) ----------------------------
const TABLES = [
  {
    id: 'brands', name: 'Brands', rowSecurity: true,
    columns: [
      id('team_id'),
      str('label_name', 255, { required: true }),
      str('produkte', 1024), str('zielgruppe', 1024),
      str('brand_worte', 64, { array: true }),
      str('am_markt_seit', 64),
      str('stripe_customer_id', 64),
      int('default_video_count', { min: 3, max: 6, default: 4 }),
      enm('status', ['trial', 'aktiv', 'pausiert'], { default: 'trial' }),
    ],
    indexes: [{ key: 'idx_team', type: 'key', columns: ['team_id'] }],
  },
  {
    // 🔒 Dev-Modus: nur internes Team
    id: 'rules', name: 'Rules', rowSecurity: false, permissions: internalOnly,
    columns: [
      id('brand_id'), // null = global für alle Brands
      str('titel', 255, { required: true }),
      md('prompt_text'),
      bool('aktiv', { default: true }),
      int('sortierung', { default: 0 }),
    ],
    indexes: [{ key: 'idx_brand', type: 'key', columns: ['brand_id'] }],
  },
  {
    // 🔒 Dev-Modus: nur internes Team; Prompts sind versioniert
    id: 'prompt_templates', name: 'Prompt Templates', rowSecurity: false, permissions: internalOnly,
    columns: [
      enm('key', P_KEYS, { required: true }),
      int('version', { min: 1, default: 1 }),
      md('static_core_md'),
      jsonCol('slots'),
      enm('model_adapter', ['seedance', 'veo', 'sora', 'wan']), // null = kein Adapter
      bool('aktiv', { default: true }),
    ],
    // model_adapter gehört in den Index: P8 hat 5 Zeilen (Kern + 4 Adapter) je Version.
    // MariaDB erlaubt mehrere NULLs im Unique-Index – Eindeutigkeit der Kern-Zeilen
    // (model_adapter=null) sichert das Seed-Skript (einziger Schreiber).
    indexes: [{ key: 'uq_key_adapter_version', type: 'unique', columns: ['key', 'model_adapter', 'version'] }],
  },
  {
    id: 'categories', name: 'Categories', rowSecurity: true,
    columns: [
      id('brand_id', { required: true }),
      str('name', 64, { required: true }), // location, licht, farben, kamera, voice, geraeusche, texte-hooks, handlungen
      int('avg_score', { min: 0, max: 10000, default: 5000 }), // Cache – per Function aktualisiert
      int('attribute_count', { default: 0 }),
    ],
    indexes: [{ key: 'idx_brand_name', type: 'key', columns: ['brand_id', 'name'] }],
  },
  {
    id: 'attributes', name: 'Attributes', rowSecurity: true,
    columns: [
      id('brand_id', { required: true }),
      id('category_id', { required: true }),
      id('parent_id'), // Unter-Attribute (self-reference)
      str('name', 255, { required: true }),
      str('slug', 255, { required: true }),
      enm('status', ['aktiv', 'archiviert'], { default: 'aktiv' }), // nie löschen
      int('score', { min: 0, max: 10000, default: 5000 }),
      int('k_factor', { default: 32 }), // 32 neu → 8 etabliert
      int('start_value', { min: 0, max: 10000 }), // Kategorie-Ø bei Anlage
      int('used_count', { default: 0 }), int('wins', { default: 0 }), int('losses', { default: 0 }),
      dt('last_used_at'),
      str('tags', 255, { array: true }),
      md('beschreibung_md'), md('essenz_md'), md('details_md'),
      str('prompt_bausteine', 1024, { array: true }),
      str('negativ_prompts', 1024, { array: true }),
    ],
    indexes: [
      { key: 'idx_topn', type: 'key', columns: ['brand_id', 'category_id', 'status', 'score'], orders: ['ASC', 'ASC', 'ASC', 'DESC'] },
      { key: 'idx_parent', type: 'key', columns: ['parent_id'] },
      { key: 'idx_slug', type: 'key', columns: ['brand_id', 'slug'] },
      // Fulltext ist auf Array-Spalten verboten → Key-Index (reicht für Query.contains)
      { key: 'idx_tags', type: 'key', columns: ['tags'] },
    ],
  },
  {
    // append-only Log – nie ändern/löschen
    id: 'score_events', name: 'Score Events', rowSecurity: true,
    columns: [
      id('attribute_id', { required: true }),
      id('video_id'), id('opponent_attribute_id'),
      enm('event_type', ['vote', 'vote_favorit_bestaetigt', 'performance', 'gezielte_frage', 'llm_vergleich'], { required: true }),
      int('delta'), int('new_score', { min: 0, max: 10000 }),
      flt('weight', { default: 1 }), // ×0,3 / ×1 / ×2
      str('kommentar', 1024), // z. B. P12-Erkenntnis
    ],
    indexes: [{ key: 'idx_attr_created', type: 'key', columns: ['attribute_id', '$createdAt'] }],
  },
  {
    id: 'assets', name: 'Assets', rowSecurity: true,
    columns: [
      id('brand_id', { required: true }),
      enm('typ', ['gesicht', 'produkt', 'logo', 'sonstiges'], { required: true }),
      str('name', 255, { required: true }),
      id('released_version_id'), // nur diese Version wird in Videos verwendet
    ],
    indexes: [{ key: 'idx_brand', type: 'key', columns: ['brand_id'] }],
  },
  {
    id: 'asset_versions', name: 'Asset Versions', rowSecurity: true,
    columns: [
      id('asset_id', { required: true }),
      int('version_no', { min: 1 }),
      enm('status', ['entwurf', 'freigegeben', 'archiviert'], { default: 'entwurf' }),
      md('beschreibung_md'),
      str('merkmale', 255, { array: true }),
      str('change_reason', 512),
      id('won_against_version_id'), // Vorher/Nachher-Vote
      id('reference_file_ids', { array: true }), // → Bucket asset-references
      id('einwilligung_file_id'), // → Bucket consents
    ],
    indexes: [{ key: 'idx_asset', type: 'key', columns: ['asset_id', 'version_no'] }],
  },
  {
    id: 'scenes', name: 'Scenes', rowSecurity: true,
    columns: [
      id('brand_id', { required: true }),
      md('user_prompt'), // Original, unverändert aufheben
      jsonCol('selected_context', 65535), // was P4 injiziert hat (inkl. Scores zum Zeitpunkt)
      md('script_md'), md('script_final_md'), // Diff = P6-Signal
      enm('status', ['entwurf', 'script', 'generiert', 'voting', 'fertig'], { default: 'entwurf' }), // Realtime-Kanal fürs UI
      int('video_count', { min: 1, default: 4 }),
    ],
    indexes: [{ key: 'idx_brand_status', type: 'key', columns: ['brand_id', 'status'] }],
  },
  {
    id: 'videos', name: 'Videos', rowSecurity: true,
    columns: [
      id('scene_id', { required: true }),
      id('brand_id', { required: true }),
      enm('ki_model', ['seedance', 'veo', 'sora', 'wan']),
      md('prompt_sent'), // exakt was ans Modell ging (Dev-Modus)
      id('storage_file_id'), // → Bucket generated-videos
      jsonCol('tags'), // P9-Analyse
      id('attribute_ids', { array: true }), // Attribution (many-to-many)
      id('asset_version_ids', { array: true }),
      bool('is_system_favorite', { default: false }),
      enm('vote_result', ['gewonnen', 'verloren']), // null = noch offen ("–")
      bool('published', { default: false }),
      str('platforms', 64, { array: true }),
      dt('published_at'),
    ],
    indexes: [
      { key: 'idx_scene', type: 'key', columns: ['scene_id'] },
      { key: 'idx_brand', type: 'key', columns: ['brand_id'] },
    ],
  },
  {
    // Zeitreihe (Cron-Function, Ads-APIs) – append-only
    id: 'video_metrics', name: 'Video Metrics', rowSecurity: true,
    columns: [
      id('video_id', { required: true }),
      dt('fetched_at'),
      flt('ctr'), flt('thumbstop'), flt('roas'), flt('follower_gained'), flt('spend'),
    ],
    indexes: [{ key: 'idx_video_fetched', type: 'key', columns: ['video_id', 'fetched_at'] }],
  },
  {
    id: 'votes', name: 'Votes', rowSecurity: true,
    columns: [
      id('scene_id', { required: true }),
      id('winner_video_id', { required: true }),
      bool('favorit_bestaetigt', { default: false }), // → Gewichtung ×0,3
    ],
    indexes: [{ key: 'idx_scene', type: 'key', columns: ['scene_id'] }],
  },
  {
    // gezielte Fragen (P13)
    id: 'questions', name: 'Questions', rowSecurity: true,
    columns: [
      id('brand_id', { required: true }),
      id('scene_id'),
      str('frage', 1024, { required: true }),
      id('attribute_a_id'), id('attribute_b_id'),
      enm('antwort', ['a', 'b', 'egal', 'offen'], { default: 'offen' }),
    ],
    indexes: [{ key: 'idx_brand_antwort', type: 'key', columns: ['brand_id', 'antwort'] }],
  },
  {
    // jede KI-Aktion als Zeile (Warteschlange + Kostenkontrolle) – append-only
    id: 'jobs', name: 'Jobs', rowSecurity: true,
    columns: [
      id('brand_id', { required: true }),
      enm('typ', ['bild_gen', 'video_gen', 'tagging', 'vergleich', 'script'], { required: true }),
      str('prompt_template_key', 16), // welcher Prompt …
      int('prompt_template_version'), // … in welcher Version lief
      enm('status', ['wartend', 'laeuft', 'fertig', 'fehler'], { default: 'wartend' }),
      flt('cost_usd'), int('tokens'),
      str('error', 4096),
      jsonCol('refs', 8192),
    ],
    indexes: [
      { key: 'idx_brand_status', type: 'key', columns: ['brand_id', 'status'] },
      { key: 'idx_queue', type: 'key', columns: ['status', '$createdAt'] },
    ],
  },
  {
    // für Stripe (metered billing: Preis pro Video)
    id: 'usage_records', name: 'Usage Records', rowSecurity: true,
    columns: [
      id('brand_id', { required: true }),
      str('periode', 16), // z. B. "2026-07"
      int('videos_generiert', { default: 0 }),
      flt('kosten_usd', { default: 0 }),
    ],
    indexes: [{ key: 'uq_brand_periode', type: 'unique', columns: ['brand_id', 'periode'] }],
  },
];

const BUCKETS = [
  { id: 'uploads', name: 'Uploads (Onboarding)' },
  { id: 'asset-references', name: 'Asset-Referenzbilder' },
  { id: 'generated-videos', name: 'Generierte Videos' },
  { id: 'consents', name: 'Einwilligungen', permissions: [`read("${INTERNAL}")`] },
];

// ---- Ablauf ---------------------------------------------------------------
async function waitForColumn(tableId, key) {
  for (let i = 0; i < 30; i++) {
    const { status, json } = await api('GET', `/tablesdb/${DB_ID}/tables/${tableId}/columns/${key}`);
    if (status === 200 && json.status === 'available') return true;
    if (status === 200 && json.status === 'failed') {
      console.warn(`  ! Spalte ${tableId}.${key}: Status "failed"`);
      warned++;
      return false;
    }
    await new Promise(r => setTimeout(r, 1000));
  }
  console.warn(`  ! Spalte ${tableId}.${key}: nicht verfügbar nach 30s`);
  warned++;
  return false;
}

async function main() {
  console.log(`Endpoint: ${ENDPOINT}\nProjekt:  ${PROJECT}\nDB:       ${DB_ID}\n`);

  console.log('Team:');
  await ensure('internal (Internes Team / Dev-Modus)', 'POST', '/teams', { teamId: 'internal', name: 'Internes Team (Dev)' });

  console.log('Datenbank:');
  await ensure(`${DB_ID}`, 'POST', '/tablesdb', { databaseId: DB_ID, name: 'BrandLoop' });

  for (const t of TABLES) {
    console.log(`Tabelle ${t.id}:`);
    await ensure(t.id, 'POST', `/tablesdb/${DB_ID}/tables`, {
      tableId: t.id, name: t.name,
      permissions: t.permissions || [],
      rowSecurity: t.rowSecurity,
    });
    for (const c of t.columns) {
      // Erst GET: Appwrite prüft das Zeilengrößen-Limit VOR dem Duplikat-Check,
      // ein erneutes POST auf große Spalten gäbe sonst 400 statt 409.
      const { status } = await api('GET', `/tablesdb/${DB_ID}/tables/${t.id}/columns/${c.body.key}`);
      if (status === 200) { skipped++; console.log(`  = ${t.id}.${c.body.key} (existiert)`); continue; }
      await ensure(`${t.id}.${c.body.key}`, 'POST', `/tablesdb/${DB_ID}/tables/${t.id}/columns/${c.type}`, c.body);
    }
  }

  // Indizes erst, wenn alle beteiligten Spalten "available" sind
  for (const t of TABLES) {
    if (!t.indexes?.length) continue;
    console.log(`Indizes ${t.id}:`);
    const needed = new Set(t.indexes.flatMap(i => i.columns).filter(c => !c.startsWith('$')));
    for (const key of needed) await waitForColumn(t.id, key);
    for (const idx of t.indexes) {
      const body = { key: idx.key, type: idx.type, columns: idx.columns, orders: idx.orders };
      if (idx.optional) await tryEnsure(`${t.id}.${idx.key}`, 'POST', `/tablesdb/${DB_ID}/tables/${t.id}/indexes`, body);
      else await ensure(`${t.id}.${idx.key}`, 'POST', `/tablesdb/${DB_ID}/tables/${t.id}/indexes`, body);
    }
  }

  console.log('Storage-Buckets:');
  for (const b of BUCKETS) {
    await ensure(b.id, 'POST', '/storage/buckets', {
      bucketId: b.id, name: b.name,
      fileSecurity: true,
      permissions: b.permissions || [],
      enabled: true,
    });
  }

  console.log(`\nFertig: ${created} angelegt, ${skipped} existierten schon, ${warned} Warnung(en).`);
}

main().catch(e => { console.error(`\nAbbruch: ${e.message}`); process.exit(1); });
