// CortexAPI client — Frontend-wrapper für Firebase Function Endpoints.
//
// Backend: /api/scraper/** (Firebase Functions v2 in europe-west3)
// Routing: Firebase Hosting rewrite /api/** → function "api"
//
// Auth (V0): X-Cortex-User header = hardcoded test-uid. Production:
// Firebase Auth ID-Token via Authorization: Bearer <idToken>.
//
// Usage:
//   const api = new CortexAPI();
//   const { queryProfileId, intent } = await api.createProfile({ name, query });
//   await api.runProfile(queryProfileId);
//   const { leads } = await api.listLeads(queryProfileId);

const CORTEX_API_BASE = '/api/scraper';

// V0 test-uid — Janik's bootstrap-admin account. Replace with Firebase Auth
// ID-Token in Bauphase 2b.5 once Auth-Wiring im Produkt steht.
const V0_TEST_USER_ID = 'xBJ0DFd0v8xES4QOFBnLRC4OxzbD';

// User-Id-Resolution kennt drei Modi:
//   1. ?impersonate=<uid>  — Admin-Test-Account (vom Admin-Panel "Open as user").
//                            Persistent im sessionStorage des Tabs damit F5
//                            den Account behält.
//   2. ?fresh=1            — Anonymer Demo-User mit eigener Wegwerf-UID.
//                            Auch sessionStorage-persistent.
//   3. (default)           — V0_TEST_USER_ID (Janik's bootstrap-admin-Account).
//
// In Bauphase 2b wird das durch echte Firebase-Auth-ID-Tokens ersetzt;
// Impersonation läuft dann über signInWithCustomToken statt URL-Param.
function resolveUserId() {
  if (typeof window === 'undefined') return V0_TEST_USER_ID;
  try {
    const params = new URLSearchParams(window.location.search);
    // Impersonation hat Vorrang. Tab merkt sich die UID via sessionStorage,
    // damit der Tab nach F5 immer noch der Test-Account ist (statt zurück
    // auf den eigenen Admin-Account zu fallen).
    const impersonateKey = 'ls.impersonateUid';
    if (params.has('impersonate')) {
      const uid = params.get('impersonate')?.trim();
      if (uid) {
        window.sessionStorage.setItem(impersonateKey, uid);
        return uid;
      }
    }
    const persistedImpersonation = window.sessionStorage.getItem(impersonateKey);
    if (persistedImpersonation) return persistedImpersonation;

    if (params.has('fresh')) {
      const key = 'ls.freshUid';
      let uid = window.sessionStorage.getItem(key);
      if (!uid) {
        uid = 'fresh_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36).slice(-4);
        window.sessionStorage.setItem(key, uid);
      }
      return uid;
    }
  } catch {
    /* fallthrough */
  }
  return V0_TEST_USER_ID;
}

const ACTIVE_USER_ID = resolveUserId();
const IS_FRESH_USER = ACTIVE_USER_ID.startsWith('fresh_');
const IS_IMPERSONATED = (() => {
  if (typeof window === 'undefined') return false;
  try { return !!window.sessionStorage.getItem('ls.impersonateUid'); } catch { return false; }
})();

class CortexAPI {
  constructor(userId = ACTIVE_USER_ID) {
    this.userId = userId;
    this.isFresh = IS_FRESH_USER;
    this.isImpersonated = IS_IMPERSONATED;
  }

  async _fetch(path, options = {}) {
    // Belt-and-suspenders: GETs get a `_t` cache-buster so Firebase Hosting's
    // edge CDN can't serve a stale 4xx/5xx for a live response. The API
    // middleware already sets Cache-Control: no-store, but Firebase sometimes
    // overrides with max-age=600, which has bitten us before.
    const isGet = !options.method || options.method === 'GET';
    const sep = path.includes('?') ? '&' : '?';
    const url = CORTEX_API_BASE + path + (isGet ? `${sep}_t=${Date.now()}` : '');
    const res = await fetch(url, {
      ...options,
      cache: isGet ? 'no-store' : (options.cache || 'default'),
      headers: {
        'Content-Type': 'application/json',
        'X-Cortex-User': this.userId,
        ...(options.headers || {}),
      },
    });
    if (!res.ok) {
      const text = await res.text().catch(() => '<unreadable>');
      throw new Error(`[cortex-api] ${res.status} ${path}: ${text}`);
    }
    return res.json();
  }

  // --- Profiles ---
  async createProfile({ name, query, cadence = 'manual', creditsPerRun = 10 }) {
    return this._fetch('/profiles', {
      method: 'POST',
      body: JSON.stringify({ name, query, cadence, creditsPerRun }),
    });
  }

  // Returns { profiles: [...] }. Pushes server-issued qp_* profiles into the UI.
  async listProfiles() {
    return this._fetch('/profiles');
  }

  // Convenience: shape a server-lead into the Prototype's flat lead-model
  // used by LeadsTable/ScreenDetail. Backend uses nested schemas (company.name,
  // contacts[0], cortex.justification); prototype uses flat fields.
  static toPrototypeLead(serverLead) {
    const primary = (serverLead.contacts && serverLead.contacts[0]) || {};
    const city = serverLead.address?.city || '';
    const region = serverLead.address?.region || '';
    const cityDisplay = [city, region].filter(Boolean).join(', ') || region || city || '—';
    const legalForm = serverLead.company?.legalForm && serverLead.company.legalForm !== 'unknown'
      ? serverLead.company.legalForm
      : '';
    const sector = [serverLead.classification?.primarySector, legalForm].filter(Boolean).join(' · ')
      || serverLead.classification?.primarySector
      || '—';
    const confidence = serverLead.cortex?.confidence ?? 0;
    const score = confidence >= 0.85 ? 'sehr sicher'
      : confidence >= 0.7 ? 'eingeschränkt sicher'
      : confidence >= 0.5 ? 'vorsichtig optimistisch'
      : 'unsicher';
    const sizeLabel = serverLead.size?.employeeEstimate
      ? `${serverLead.size.employeeEstimate} Mitarbeiter`
      : serverLead.size?.employeeRange && serverLead.size.employeeRange !== 'unknown'
        ? `${serverLead.size.employeeRange} Mitarbeiter`
        : '—';
    return {
      id: serverLead.id,
      company: serverLead.company?.name || serverLead.domain || 'Unbekannt',
      sector,
      city: cityDisplay,
      size: sizeLabel,
      founded: serverLead.company?.foundedYear ? String(serverLead.company.foundedYear) : '—',
      website: serverLead.web?.website || serverLead.domain || '',
      decider: {
        name: primary.fullName || '—',
        role: primary.role?.title ? primary.role.title.replace(/_/g, ' ') : '—',
        email: primary.email || '',
      },
      excerpt: serverLead.company?.description?.slice(0, 280) || '',
      reason: serverLead.cortex?.justification || '',
      reasonRefs: [],
      uncertainty: null,
      score,
      isNew: true,
      // preserve raw payload for any downstream component that needs it
      _server: serverLead,
    };
  }

  // Convenience: shape a server-profile into the Prototype's profile-model so
  // it drops into the existing UI dropdown without extra glue.
  static toPrototypeProfile(serverProfile) {
    const name = serverProfile.name || 'Cortex-Profil';
    const intent = serverProfile.intent || {};
    const queryText = [intent.businessType, intent.region, intent.employeeRange]
      .filter(Boolean)
      .join(' · ') || intent.rawQuery || 'Cortex-Profil';
    const createdAt = serverProfile.createdAt
      ? new Date(serverProfile.createdAt).toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric' })
      : '—';
    // Pull live counters from the profile's currentState so the Dashboard
    // row + Reifegrad-Chip show reality, not hardcoded zeros. The server
    // updates these transactionally on every feedback via /feedback.
    const state = serverProfile.currentState || {};
    const totalFeedbacks = state.totalFeedbacks ?? 0;
    const totalAccepted = state.totalAccepted ?? 0;
    const totalRejected = state.totalRejected ?? 0;
    const matchRate = state.matchRate ?? (totalFeedbacks > 0 ? totalAccepted / totalFeedbacks : 0);
    return {
      id: serverProfile.queryProfileId,
      name,
      short: name.length > 24 ? `${name.slice(0, 22)}…` : name,
      initial: name.trim().charAt(0).toUpperCase() || 'C',
      query: queryText,
      createdAt,
      feedbackCount: totalFeedbacks,
      acceptedCount: totalAccepted,
      rejectedCount: totalRejected,
      matchRateNow: matchRate,
      matchRateStart: 0,
      confidenceLevel: state.confidenceLevel ?? 'cold_start',
      leads: [],
      memory: [],
      isServerProfile: true,
      serverIntent: intent,
      // Schedule info lifted up so the Dashboard row + profile-header badge
      // can render "Nächster Lauf in X" / "Täglich · pausiert" without
      // re-fetching the profile doc. Kept as raw backend shape.
      schedule: serverProfile.schedule || null,
    };
  }

  // Schedule: set cadence so the scheduler-worker fires the orchestrator
  // automatically. `preferredHourUtc` is optional (0-23) — if null, next run
  // fires 5 min after the toggle; after that the cadence-gap takes over.
  async setSchedule(profileId, { cadence, preferredHourUtc, paused }) {
    return this._fetch(`/profiles/${profileId}/schedule`, {
      method: 'PATCH',
      body: JSON.stringify({ cadence, preferredHourUtc, paused }),
    });
  }

  // --- Runs ---
  // 2026-04-22: 2. Param `leadQuery` ist die konkrete Suchquery
  // (Unternehmensart/Ort/Region/Ansprechpartner/Zusatz-Freitext) die im
  // RunModal eingegeben wurde. Backend (Phase 2) nimmt sie als Override
  // statt das Profile-Intent. Wenn nicht übergeben → fallback auf
  // existing Profile-Intent (= aktuelles Verhalten).
  async runProfile(profileId, leadQuery) {
    return this._fetch(`/profiles/${profileId}/run`, {
      method: 'POST',
      body: leadQuery ? JSON.stringify({ leadQuery }) : undefined,
    });
  }

  async listRuns(profileId) {
    return this._fetch(`/profiles/${profileId}/runs`);
  }

  // Maturity / Reifegrad — mirrors ConfidenceLevel in schemas.userMemory.
  // Thresholds must match backend (orchestrator memoryNarrative + feedback
  // endpoint) so the server- and client-side labels stay consistent.
  // Returns { stage, label, nextStage, needed, description }.
  static getMaturity(totalFeedbacks = 0) {
    if (totalFeedbacks >= 50) {
      return {
        stage: 'mature',
        label: 'Ausgereift',
        description: 'Cortex behandelt die Muster fast wie Regeln.',
        nextStage: null,
        needed: 0,
      };
    }
    if (totalFeedbacks >= 25) {
      return {
        stage: 'calibrated',
        label: 'Kalibriert',
        description: 'Cortex nutzt deine Muster als starkes Signal — darf Entscheidungen flippen.',
        nextStage: 'mature',
        needed: 50 - totalFeedbacks,
      };
    }
    if (totalFeedbacks >= 10) {
      return {
        stage: 'early',
        label: 'Einlernend',
        description: 'Cortex sieht erste Muster — soft als Signal, kann Edge-Cases kippen.',
        nextStage: 'calibrated',
        needed: 25 - totalFeedbacks,
      };
    }
    return {
      stage: 'cold_start',
      label: 'Cold-Start',
      description: 'Noch zu wenig Feedback — Cortex sammelt Evidenz, bestraft noch keine Muster.',
      nextStage: 'early',
      needed: 10 - totalFeedbacks,
    };
  }

  // Apply an email-pattern template to a contact's name → concrete email
  // string. Used by the "Pattern anwenden"-button in the Lead-Detail view
  // for contacts whose direct email wasn't captured during hydration.
  // Returns null when the template requires a token we can't fill.
  static applyEmailPattern(pattern, firstName, lastName) {
    if (!pattern) return null;
    const fold = (s) => (s || '')
      .replace(/ä/gi, (c) => (c === 'ä' ? 'ae' : 'Ae'))
      .replace(/ö/gi, (c) => (c === 'ö' ? 'oe' : 'Oe'))
      .replace(/ü/gi, (c) => (c === 'ü' ? 'ue' : 'Ue'))
      .replace(/ß/g, 'ss')
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .toLowerCase();
    const f = fold(firstName);
    const l = fold(lastName);
    if (pattern.includes('{first}') && !f) return null;
    if (pattern.includes('{last}') && !l) return null;
    if (pattern.includes('{f}') && !f) return null;
    if (pattern.includes('{l}') && !l) return null;
    return pattern
      .replace('{first}', f)
      .replace('{last}', l)
      .replace('{f}', f.charAt(0))
      .replace('{l}', l.charAt(0));
  }

  // User-friendly guidance for a completed/failed run. Returns null if the
  // run is still going or the result was a normal healthy delivery (>=1 lead).
  // Used by the Dashboard row-chip + Leads-Tab empty-state so users never
  // land on a dead-end after a failed search. Plain-German on purpose.
  static buildRunGuidance(run) {
    if (!run) return null;
    if (run.status === 'running') return null;
    if (run.status === 'failed') {
      return {
        kind: 'error',
        title: 'Cortex ist hängen geblieben',
        body: 'Technischer Aussetzer beim letzten Suchlauf. Warte 1–2 Minuten und probier es nochmal. Wenn es wieder hängt, sag uns Bescheid.',
        ctaLabel: 'Nochmal versuchen',
        ctaKind: 'retry',
      };
    }
    if (run.status === 'completed') {
      const scanned = run.candidatesScanned ?? 0;
      const reasoned = run.candidatesReasoned ?? 0;
      const delivered = run.candidatesDelivered ?? 0;
      if (delivered >= 1) return null; // happy path
      if (scanned === 0) {
        return {
          kind: 'no-results',
          title: 'Zu deiner Query fanden sich keine Treffer im Web',
          body: 'Deine Keywords liefern auf Google keine passenden deutschen B2B-Firmen. Versuche alltäglichere Begriffe — z.B. „Schreinerei" statt „Möbelmanufaktur", oder erweitere die Region.',
          ctaLabel: 'Query anpassen',
          ctaKind: 'editQuery',
        };
      }
      if (reasoned > 0 && delivered === 0) {
        return {
          kind: 'too-strict',
          title: `${reasoned} Kandidaten geprüft — keiner passte zu deinem Profil`,
          body: 'Cortex fand Firmen, aber keine davon matcht deine Zielgruppe sauber. Vielleicht ist deine Beschreibung zu eng — nimm z.B. eine größere Region, lockere die Mitarbeiterzahl oder lass Details raus.',
          ctaLabel: 'Query breiter formulieren',
          ctaKind: 'editQuery',
        };
      }
      return {
        kind: 'empty',
        title: 'Keine passenden Leads geliefert',
        body: 'Dieser Run war ergebnislos. Versuche es nochmal mit anderen Keywords oder einer lockereren Beschreibung.',
        ctaLabel: 'Nochmal versuchen',
        ctaKind: 'retry',
      };
    }
    return null;
  }

  async getRun(runId) {
    return this._fetch(`/runs/${runId}`);
  }

  // --- Leads ---
  async listLeads(profileId) {
    return this._fetch(`/profiles/${profileId}/leads`);
  }

  // --- Feedback ---
  // Sends an accept/reject on a lead. Backend persists it + appends a memory
  // event + updates the profile's rolling feedback counters. This is what
  // turns Cortex from "Google with Claude" into a learning agent.
  async sendFeedback(leadId, { action, primaryReason, comment } = {}) {
    return this._fetch(`/leads/${leadId}/feedback`, {
      method: 'POST',
      body: JSON.stringify({ action, primaryReason, comment }),
    });
  }

  // --- Notifications ---
  async listNotifications() {
    return this._fetch('/notifications');
  }

  async markNotificationRead(id) {
    return this._fetch(`/notifications/${id}/read`, { method: 'POST' });
  }
}

// Global singleton — attached to window for access from any jsx file.
// The static helper `toPrototypeProfile` is promoted to an instance method so
// each jsx file can call `window.CortexAPI.toPrototypeProfile(...)` without
// needing the class itself in scope.
const _cortexApi = new CortexAPI();
_cortexApi.toPrototypeProfile = CortexAPI.toPrototypeProfile;
_cortexApi.toPrototypeLead = CortexAPI.toPrototypeLead;
_cortexApi.buildRunGuidance = CortexAPI.buildRunGuidance;
_cortexApi.applyEmailPattern = CortexAPI.applyEmailPattern;
_cortexApi.getMaturity = CortexAPI.getMaturity;
window.CortexAPI = _cortexApi;
