import { BaseService, type BaseServiceContext } from '@learnitive/base-utils';
import { SERVICE_NAME } from '@learnitive/constants';
import { atom } from 'jotai';
import type { ProjectFilePath, LearnitiveCompiler } from '@learnitive/project-storage';

const TOKEN_KEY = 'learnitive:pro:entitlement:v1';
const LEGACY_PUBLIC_KEY_KEY = 'learnitive:pro:public-key:v1';
const TOKEN_PREFIX = 'learnitive1';
const CLOUD_FALLBACK_CLIENT_KEY = 'learnitive:compile:fallback-client:v1';
const DEFAULT_FREE_PROJECT_LIMIT = 5;

export type ProjectCapability =
  | 'ai.chat'
  | 'ai.agent'
  | 'ai.cloud'
  | 'compile.cloud'
  | 'terminal.linux'
  | 'templates.pro'
  | 'collaboration.advanced'
  | 'review.advanced'
  | 'research.paper-memory'
  | 'export.pdf.clean';

export type ProjectEntitlementPayload = {
  version: 1;
  tier: 'pro';
  source: 'payhip' | 'predefined';
  licenseType?: 'lifetime' | 'monthly' | 'predefined';
  expiresAt?: number;
  licenseFingerprint: string;
  issuedAt: number;
  refreshAfter: number;
  validUntil: number;
  refreshHandle: string;
  kid?: string;
};

export type ProjectEntitlementState = {
  status: 'loading' | 'free' | 'pro' | 'error';
  tier: 'free' | 'pro';
  payload?: ProjectEntitlementPayload;
  accountSource?: 'admin' | 'license';
  accountExpiresAt?: number;
  error?: string;
  refreshing: boolean;
};

type AccountAuthority =
  | { authenticated: false }
  | {
      authenticated: true;
      csrfToken: string;
      entitlement?: {
        pro?: boolean;
        blocked?: boolean;
        expired?: boolean;
        source?: 'admin' | 'license' | 'none';
        projectLimit?: number | null;
        quota?: number;
        usage?: number;
        remaining?: number;
        license?: {
          type?: 'lifetime' | 'monthly' | 'predefined';
          expiresAt?: string | null;
          expired?: boolean;
        };
      };
    };

export type AiReviewQuotaStatus = { used: number; remaining: number; limit: number; unlimited?: boolean; source?: 'admin' | 'license' | 'byok' };
export type AiReviewQuotaReservation = AiReviewQuotaStatus & { ok: true; reservationId: string };
export type AiReviewServerCapabilities = { adminOpenAiFallback: boolean; provider?: string };
export type AiReviewRefundReason =
  | 'extraction_failure'
  | 'missing_provider'
  | 'provider_failure_before_generation'
  | 'entitlement_failure'
  | 'cancelled_before_generation';


export type CloudCompileCapabilities = {
  enabled: boolean;
  allowedEngines: LearnitiveCompiler[];
  maxFiles: number;
  maxProjectBytes: number;
  maxCompileSeconds: number;
  maxOutputBytes: number;
  concurrency: number;
  dailyAllowance: number;
  monthlyAllowance: number;
  freeFallbackEnabled: boolean;
};

export type CloudCompileResponse = {
  success: boolean;
  jobId?: string;
  pdf?: Uint8Array;
  synctex?: Uint8Array;
  log: string;
  errors: Array<{line?: number; message?: string; type?: string}>;
  effectiveEngine?: LearnitiveCompiler;
  engineFallbackUsed?: boolean;
};

function bytesToBase64(bytes: Uint8Array): string {
  let binary = '';
  for (let offset = 0; offset < bytes.length; offset += 0x8000) {
    binary += String.fromCharCode(...bytes.subarray(offset, Math.min(bytes.length, offset + 0x8000)));
  }
  return btoa(binary);
}

function base64ToBytes(value: string): Uint8Array {
  const binary = atob(value);
  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
}

function cloudCompileFailureMessage(code: string, status: number, fallback: boolean): string {
  const fallbackPrefix = fallback ? 'Full TeX Live fallback' : 'Cloud Compile';
  switch (code) {
    case 'cloud_compile_fallback_disabled': return 'Full TeX Live fallback is disabled on this Learnitive deployment.';
    case 'untrusted_origin': return `${fallbackPrefix} request was rejected because the application origin is not trusted.`;
    case 'invalid_csrf': return `${fallbackPrefix} request authorization expired. Refresh the page and try again.`;
    case 'authentication_required': return `${fallbackPrefix} authorization could not be established.`;
    case 'cloud_compile_disabled': return 'Cloud Compile is disabled on this Learnitive deployment.';
    case 'cloud_compile_busy': return `${fallbackPrefix} is busy. Try again shortly.`;
    case 'cloud_compile_timeout': return `${fallbackPrefix} timed out.`;
    case 'cloud_compile_project_too_large': return 'This project exceeds the Cloud Compile upload limit.';
    case 'cloud_compile_output_too_large': return 'Cloud Compile output is too large.';
    case 'cloud_compile_daily_allowance_exhausted':
    case 'cloud_compile_monthly_allowance_exhausted': return fallback ? 'The Full TeX Live fallback allowance has been reached.' : 'Your Cloud Compile allowance has been reached.';
    case 'runtime_image_missing': return 'Full TeX Live runtime image is unavailable on this deployment.';
    case 'runtime_docker_unavailable': return 'Full TeX Live runtime is temporarily unavailable.';
    case 'runtime_workspace_denied': return 'Full TeX Live could not create its isolated workspace.';
    case 'runtime_timeout': return 'Full TeX Live compilation timed out.';
    case 'runtime_resource_limit': return 'Full TeX Live exceeded the configured runtime resource limit.';
    case 'cloud_compile_unavailable': return `${fallbackPrefix} is temporarily unavailable.`;
    default:
      if (status === 401) return `${fallbackPrefix} authorization could not be established.`;
      if (status === 403) return `${fallbackPrefix} request was denied (${code}).`;
      if (status === 413) return 'Cloud Compile input or output is too large.';
      if (status === 429) return fallback ? 'The Full TeX Live fallback allowance has been reached.' : 'Your Cloud Compile allowance has been reached.';
      if (status === 503) return `${fallbackPrefix} is busy or temporarily unavailable.`;
      if (status === 504) return `${fallbackPrefix} timed out.`;
      return code;
  }
}

const FREE_STATE: ProjectEntitlementState = {
  status: 'free',
  tier: 'free',
  refreshing: false,
};

function base64UrlBytes(value: string): Uint8Array {
  const base64 = value.replace(/-/gu, '+').replace(/_/gu, '/');
  const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
  const binary = atob(padded);
  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
}

function copyToArrayBuffer(bytes: Uint8Array): ArrayBuffer {
  const copy = new Uint8Array(bytes.byteLength);
  copy.set(bytes);
  return copy.buffer;
}

function decodePayload(value: string): ProjectEntitlementPayload {
  const bytes = base64UrlBytes(value);
  const payload = JSON.parse(new TextDecoder().decode(bytes)) as Partial<ProjectEntitlementPayload>;
  if (
    payload.version !== 1 ||
    payload.tier !== 'pro' ||
    (payload.source !== 'payhip' && payload.source !== 'predefined') ||
    typeof payload.licenseFingerprint !== 'string' ||
    typeof payload.issuedAt !== 'number' ||
    typeof payload.refreshAfter !== 'number' ||
    typeof payload.validUntil !== 'number' ||
    typeof payload.refreshHandle !== 'string' ||
    (payload.licenseType !== undefined && payload.licenseType !== 'lifetime' && payload.licenseType !== 'monthly' && payload.licenseType !== 'predefined') ||
    (payload.expiresAt !== undefined && typeof payload.expiresAt !== 'number') ||
    (payload.kid !== undefined && typeof payload.kid !== 'string')
  ) {
    throw new Error('Invalid Learnitive entitlement payload.');
  }
  return payload as ProjectEntitlementPayload;
}

function pemToDer(pem: string): Uint8Array {
  const body = pem
    .replace(/-----BEGIN PUBLIC KEY-----/gu, '')
    .replace(/-----END PUBLIC KEY-----/gu, '')
    .replace(/\s+/gu, '');
  if (!body) throw new Error('Invalid Learnitive entitlement public key.');
  const binary = atob(body);
  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
}

function trustedEntitlementPublicKey(): string {
  try {
    return typeof __LEARNITIVE_ENTITLEMENT_PUBLIC_KEY__ === 'string'
      ? __LEARNITIVE_ENTITLEMENT_PUBLIC_KEY__.trim()
      : '';
  } catch {
    return '';
  }
}

function trustedEntitlementKeyId(): string {
  try {
    return typeof __LEARNITIVE_ENTITLEMENT_KEY_ID__ === 'string'
      ? __LEARNITIVE_ENTITLEMENT_KEY_ID__.trim()
      : '';
  } catch {
    return '';
  }
}

export async function verifyProjectEntitlement(
  token: string,
  publicKeyPem: string,
  expectedKeyId = '',
): Promise<ProjectEntitlementPayload> {
  const parts = token.split('.');
  if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX || !parts[1] || !parts[2]) {
    throw new Error('Invalid Learnitive entitlement token.');
  }
  const publicKey = await crypto.subtle.importKey(
    'spki',
    copyToArrayBuffer(pemToDer(publicKeyPem)),
    { name: 'Ed25519' },
    false,
    ['verify'],
  );
  const data = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
  const valid = await crypto.subtle.verify(
    { name: 'Ed25519' },
    publicKey,
    copyToArrayBuffer(base64UrlBytes(parts[2])),
    copyToArrayBuffer(data),
  );
  if (!valid) throw new Error('Invalid Learnitive entitlement signature.');
  const payload = decodePayload(parts[1]);
  if (expectedKeyId && payload.kid && payload.kid !== expectedKeyId) {
    throw new Error('Learnitive entitlement was signed by an unexpected key identifier.');
  }
  return payload;
}

function isBrowserOnline(): boolean {
  return typeof navigator === 'undefined' ? false : navigator.onLine;
}

function localGet(key: string): string | undefined {
  try {
    return localStorage.getItem(key) ?? undefined;
  } catch {
    return undefined;
  }
}

function localSet(key: string, value: string): void {
  try {
    localStorage.setItem(key, value);
  } catch {
    // Pro remains usable for this session even if persistent storage is unavailable.
  }
}

function localRemove(key: string): void {
  try {
    localStorage.removeItem(key);
  } catch {
    // Ignore unavailable local storage.
  }
}

function normalizeFreeProjectLimit(value: unknown): number {
  const numeric = Number(value);
  return Number.isInteger(numeric) && numeric > 0 ? numeric : DEFAULT_FREE_PROJECT_LIMIT;
}

function cloudFallbackClientId(): string {
  const existing = localGet(CLOUD_FALLBACK_CLIENT_KEY);
  if (existing && /^[A-Za-z0-9_-]{16,128}$/u.test(existing)) return existing;
  const next = globalThis.crypto?.randomUUID?.() ?? Array.from(
    globalThis.crypto?.getRandomValues?.(new Uint8Array(16)) ?? new Uint8Array(16),
    (value) => value.toString(16).padStart(2, '0'),
  ).join('');
  localSet(CLOUD_FALLBACK_CLIENT_KEY, next);
  return next;
}

export class ProjectEntitlementService extends BaseService {
  static deps = [] as const;

  readonly $state = atom<ProjectEntitlementState>({
    status: 'loading',
    tier: 'free',
    refreshing: false,
  });

  private token = '';
  private refreshTimer: ReturnType<typeof setTimeout> | undefined;
  private restorePromise: Promise<void> = Promise.resolve();
  private freeProjectLimit = DEFAULT_FREE_PROJECT_LIMIT;
  private publicPolicyLoaded = false;
  private publicPolicyPromise: Promise<void> | undefined;

  constructor(context: BaseServiceContext, dependencies: null) {
    super(SERVICE_NAME.projectEntitlementService, context, dependencies);
  }

  hookMount(): void {
    const onOnline = () => {
      void this.refresh();
    };
    if (typeof window !== 'undefined') {
      window.addEventListener('online', onOnline);
    }
    this.addCleanup(() => {
      if (typeof window !== 'undefined') {
        window.removeEventListener('online', onOnline);
      }
      if (this.refreshTimer) clearTimeout(this.refreshTimer);
    });
    this.restorePromise = this.restore();
    void this.ensurePublicPolicy();
  }

  async whenReady(): Promise<void> {
    await this.mountPromise;
    await this.restorePromise;
  }

  isPro(): boolean {
    const state = this.store.get(this.$state);
    if (state.tier !== 'pro') return false;
    const now = Math.floor(Date.now() / 1000);
    if (state.payload && (state.payload.validUntil <= now || (state.payload.expiresAt !== undefined && state.payload.expiresAt <= now))) return false;
    if (state.accountExpiresAt !== undefined && state.accountExpiresAt <= now) return false;
    return true;
  }

  async projectLimit(): Promise<number | null> {
    await this.whenReady();
    if (this.isPro()) return null;
    await this.ensurePublicPolicy();
    return this.freeProjectLimit;
  }



  can(capability: ProjectCapability): boolean {
    if (capability === 'export.pdf.clean') return this.isPro();
    return true;
  }

  assert(capability: ProjectCapability): void {
    if (!this.can(capability)) {
      if (capability === 'export.pdf.clean') {
        throw new Error('Learnitive Pro is required for clean PDF export.');
      }
      throw new Error('This Learnitive feature is currently unavailable.');
    }
  }

  async activate(licenseKey: string): Promise<void> {
    const key = licenseKey.trim();
    if (!key) throw new Error('Enter a Learnitive Pro licence key.');
    this.store.set(this.$state, { ...this.store.get(this.$state), refreshing: true, error: undefined });
    try {
      const account = await this.fetchAccountAuthority();
      const authenticated = account?.authenticated === true;
      const response = await fetch(authenticated ? '/api/licenses/verify' : '/api/license/activate', {
        method: 'POST',
        credentials: 'same-origin',
        headers: {
          'content-type': 'application/json',
          ...(authenticated ? { 'x-learnitive-csrf': account.csrfToken } : {}),
        },
        body: JSON.stringify({ licenseKey: key }),
      });
      const body = await response.json().catch(() => ({})) as Record<string, unknown>;
      if (!response.ok || typeof body.entitlement !== 'string') {
        if (response.status === 401) throw new Error('That Learnitive licence key is not valid.');
        if (response.status === 403 && body.error === 'license_blocked') {
          throw new Error('This Learnitive licence has been blocked by the administrator.');
        }
        if (response.status === 409 && body.error === 'license_already_linked') {
          throw new Error('This Learnitive licence is already linked to another active account.');
        }
        if (response.status === 409 && body.error === 'license_code_used') {
          throw new Error('This Learnitive licence code has already been used.');
        }
        if (response.status === 409 && body.error === 'license_lifetime_already_active') {
          throw new Error('This account already has Pro Lifetime. The code was not consumed.');
        }
        if (response.status === 429) throw new Error('Too many licence attempts. Try again later.');
        throw new Error('Learnitive could not verify the licence right now.');
      }
      await this.accept(body.entitlement);
      if (authenticated) await this.syncAccountAuthority();
    } finally {
      const current = this.store.get(this.$state);
      this.store.set(this.$state, { ...current, refreshing: false });
    }
  }

  async refresh(): Promise<boolean> {
    const accountAuthoritative = await this.syncAccountAuthority();
    if (accountAuthoritative) return this.store.get(this.$state).tier === 'pro';
    if (!this.token) return false;
    const current = this.store.get(this.$state);
    this.store.set(this.$state, { ...current, refreshing: true, error: undefined });
    try {
      const response = await fetch('/api/license/refresh', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ entitlement: this.token }),
      });
      const body = await response.json().catch(() => ({}));
      if (!response.ok || typeof body.entitlement !== 'string') {
        if (response.status === 401) {
          this.deactivate('The Learnitive Pro licence is no longer valid.');
        }
        return false;
      }
      await this.accept(body.entitlement);
      return true;
    } catch {
      return false;
    } finally {
      const latest = this.store.get(this.$state);
      this.store.set(this.$state, { ...latest, refreshing: false });
    }
  }


  async cloudCompileCapabilities(): Promise<CloudCompileCapabilities> {
    try {
      const response = await fetch('/api/config/public', {credentials: 'same-origin', cache: 'no-store'});
      if (!response.ok) return {enabled: false, allowedEngines: [], maxFiles: 0, maxProjectBytes: 0, maxCompileSeconds: 0, maxOutputBytes: 0, concurrency: 0, dailyAllowance: 0, monthlyAllowance: 0, freeFallbackEnabled: false};
      const body = await response.json().catch(() => ({})) as {cloudCompile?: Partial<CloudCompileCapabilities>};
      const config = body.cloudCompile || {};
      const allowedEngines = Array.isArray(config.allowedEngines)
        ? config.allowedEngines.filter((value): value is LearnitiveCompiler => value === 'pdflatex' || value === 'xelatex' || value === 'lualatex')
        : [];
      return {
        enabled: config.enabled === true,
        allowedEngines,
        maxFiles: Number(config.maxFiles) || 0,
        maxProjectBytes: Number(config.maxProjectBytes) || 0,
        maxCompileSeconds: Number(config.maxCompileSeconds) || 0,
        maxOutputBytes: Number(config.maxOutputBytes) || 0,
        concurrency: Number(config.concurrency) || 0,
        dailyAllowance: Number(config.dailyAllowance) || 0,
        monthlyAllowance: Number(config.monthlyAllowance) || 0,
        freeFallbackEnabled: config.freeFallbackEnabled === true,
      };
    } catch {
      return {enabled: false, allowedEngines: [], maxFiles: 0, maxProjectBytes: 0, maxCompileSeconds: 0, maxOutputBytes: 0, concurrency: 0, dailyAllowance: 0, monthlyAllowance: 0, freeFallbackEnabled: false};
    }
  }

  async cloudCompile(input: {
    engine: LearnitiveCompiler;
    rootFile: ProjectFilePath;
    files: readonly {path: ProjectFilePath; content: Uint8Array}[];
    signal?: AbortSignal;
    fallback?: boolean;
  }): Promise<CloudCompileResponse> {
    const account = await this.fetchAccountAuthority();
    const accountAuthenticated = account?.authenticated === true;
    const fallback = input.fallback === true;
    const response = await fetch('/api/compile/cloud', {
      method: 'POST',
      credentials: 'same-origin',
      signal: input.signal,
      headers: {
        'content-type': 'application/json',
        ...(accountAuthenticated ? {'x-learnitive-csrf': account.csrfToken} : {}),
      },
      body: JSON.stringify({
        engine: input.engine,
        rootFile: input.rootFile,
        files: input.files.map((file) => ({name: file.path, content: bytesToBase64(file.content)})),
        ...(fallback ? {fallback: true} : {}),
        ...(!accountAuthenticated ? {fallbackClientId: cloudFallbackClientId()} : {}),
        ...(!accountAuthenticated && this.token ? {entitlement: this.token} : {}),
      }),
    });
    const body = await response.json().catch(() => ({})) as Record<string, unknown>;
    if (!response.ok) {
      if (response.status === 422 && typeof body.log === 'string') {
        return {
          success: false,
          log: body.log,
          errors: Array.isArray(body.errors) ? body.errors as CloudCompileResponse['errors'] : [],
          ...(typeof body.effectiveEngine === 'string' && (body.effectiveEngine === 'pdflatex' || body.effectiveEngine === 'xelatex' || body.effectiveEngine === 'lualatex') ? {effectiveEngine: body.effectiveEngine} : {}),
          engineFallbackUsed: body.engineFallbackUsed === true,
        };
      }
      const code = typeof body.error === 'string' ? body.error : `http_${response.status}`;
      throw new Error(cloudCompileFailureMessage(code, response.status, fallback));
    }
    if (body.success !== true || typeof body.pdf !== 'string') throw new Error('Cloud Compile returned an invalid response.');
    return {
      success: true,
      ...(typeof body.jobId === 'string' ? {jobId: body.jobId} : {}),
      pdf: base64ToBytes(body.pdf),
      ...(typeof body.synctex === 'string' ? {synctex: base64ToBytes(body.synctex)} : {}),
      log: typeof body.log === 'string' ? body.log : '',
      errors: Array.isArray(body.errors) ? body.errors as CloudCompileResponse['errors'] : [],
      ...(typeof body.effectiveEngine === 'string' && (body.effectiveEngine === 'pdflatex' || body.effectiveEngine === 'xelatex' || body.effectiveEngine === 'lualatex') ? {effectiveEngine: body.effectiveEngine} : {}),
      engineFallbackUsed: body.engineFallbackUsed === true,
    };
  }

  async aiReviewServerCapabilities(): Promise<AiReviewServerCapabilities> {
    return { adminOpenAiFallback: false };
  }

  async generateAdminAiReview(reservationId: string, review: Record<string, unknown>): Promise<{ provider: string; model: string; review: unknown }> {
    void reservationId;
    void review;
    throw new Error('Server-funded AI Review is disabled. Use Local AI or your browser-held BYOK provider.');
  }

  /** Compatibility surface: AI Review no longer consumes a Learnitive plan quota. */
  async aiReviewQuotaStatus(): Promise<AiReviewQuotaStatus> {
    return { used: 0, remaining: 0, limit: 0, unlimited: true, source: 'byok' };
  }

  async reserveAiReview(_provider: string): Promise<AiReviewQuotaReservation> {
    throw new Error('AI Review no longer uses a Learnitive quota. Use Local AI or your browser-held BYOK provider.');
  }

  async startAiReviewGeneration(_reservationId: string): Promise<void> {
    throw new Error('AI Review no longer uses server-side generation reservations.');
  }

  async completeAiReview(_reservationId: string): Promise<void> {
    throw new Error('AI Review no longer uses server-side generation reservations.');
  }

  async refundAiReview(_reservationId: string, _reason: AiReviewRefundReason): Promise<void> {
    throw new Error('AI Review no longer uses server-side generation reservations.');
  }

  deactivate(error?: string): void {
    this.token = '';
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
      this.refreshTimer = undefined;
    }
    localRemove(TOKEN_KEY);
    localRemove(LEGACY_PUBLIC_KEY_KEY);
    this.store.set(this.$state, { ...FREE_STATE, ...(error ? { status: 'error' as const, error } : {}) });
  }

  private async restore(): Promise<void> {
    this.token = localGet(TOKEN_KEY) ?? '';
    localRemove(LEGACY_PUBLIC_KEY_KEY);
    if (!this.token) {
      this.store.set(this.$state, FREE_STATE);
    } else {
      const publicKey = trustedEntitlementPublicKey();
      if (!publicKey) {
        this.store.set(this.$state, {
          ...FREE_STATE,
          status: 'error',
          error: 'This Learnitive build has no pinned entitlement verification key.',
        });
      } else {
        try {
          const payload = await verifyProjectEntitlement(
            this.token,
            publicKey,
            trustedEntitlementKeyId(),
          );
          const now = Math.floor(Date.now() / 1000);
          if (now >= payload.validUntil) {
            this.store.set(this.$state, FREE_STATE);
          } else {
            this.store.set(this.$state, { status: 'pro', tier: 'pro', payload, refreshing: false });
            this.scheduleRefresh(payload);
          }
        } catch {
          this.token = '';
          localRemove(TOKEN_KEY);
          this.store.set(this.$state, FREE_STATE);
        }
      }
    }
    const accountAuthoritative = await this.syncAccountAuthority();
    if (!accountAuthoritative && this.token) {
      const payload = this.store.get(this.$state).payload;
      const now = Math.floor(Date.now() / 1000);
      if (payload && now >= payload.refreshAfter && isBrowserOnline()) void this.refresh();
    }
  }

  private async ensurePublicPolicy(): Promise<void> {
    if (this.publicPolicyLoaded) return;
    if (!this.publicPolicyPromise) {
      this.publicPolicyPromise = (async () => {
        try {
          const response = await fetch('/api/config/public', { credentials: 'same-origin', cache: 'no-store' });
          if (response.ok) {
            const body = await response.json().catch(() => ({})) as { freePlanProjectLimit?: unknown };
            this.freeProjectLimit = normalizeFreeProjectLimit(body.freePlanProjectLimit);
          }
        } catch {
          // The browser-local fallback remains conservative when the public config is unavailable.
        } finally {
          this.publicPolicyLoaded = true;
          this.publicPolicyPromise = undefined;
        }
      })();
    }
    await this.publicPolicyPromise;
  }

  private async fetchAccountAuthority(): Promise<AccountAuthority | undefined> {
    try {
      const response = await fetch('/api/auth/me', { credentials: 'same-origin', cache: 'no-store' });
      if (!response.ok) return undefined;
      const body = await response.json().catch(() => ({})) as Partial<AccountAuthority>;
      if (body.authenticated === false) return { authenticated: false };
      if (body.authenticated === true && typeof (body as { csrfToken?: unknown }).csrfToken === 'string') {
        return body as AccountAuthority;
      }
      return undefined;
    } catch {
      return undefined;
    }
  }

  private applyAccountAuthority(account: Extract<AccountAuthority, { authenticated: true }>): void {
    const pro = account.entitlement?.pro === true && account.entitlement?.blocked !== true;
    const parsedExpiry = account.entitlement?.license?.expiresAt ? Date.parse(account.entitlement.license.expiresAt) : Number.NaN;
    const accountExpiresAt = Number.isFinite(parsedExpiry) ? Math.floor(parsedExpiry / 1000) : undefined;
    this.store.set(this.$state, pro
      ? {
          status: 'pro',
          tier: 'pro',
          refreshing: false,
          accountSource: account.entitlement?.source === 'admin' ? 'admin' : 'license',
          ...(accountExpiresAt !== undefined ? { accountExpiresAt } : {}),
        }
      : { ...FREE_STATE, ...(account.entitlement?.blocked ? { error: 'This Learnitive Pro entitlement is blocked.' } : {}) });
  }

  private async syncAccountAuthority(): Promise<boolean> {
    const account = await this.fetchAccountAuthority();
    if (account?.authenticated !== true) return false;
    this.applyAccountAuthority(account);
    return true;
  }

  private async accept(token: string): Promise<void> {
    const publicKey = trustedEntitlementPublicKey();
    if (!publicKey) throw new Error('This Learnitive build has no pinned entitlement verification key.');
    const payload = await verifyProjectEntitlement(token, publicKey, trustedEntitlementKeyId());
    const now = Math.floor(Date.now() / 1000);
    if (payload.validUntil <= now) throw new Error('The Learnitive entitlement is already expired.');
    this.token = token;
    localSet(TOKEN_KEY, token);
    localRemove(LEGACY_PUBLIC_KEY_KEY);
    this.store.set(this.$state, { status: 'pro', tier: 'pro', payload, refreshing: false });
    this.scheduleRefresh(payload);
  }

  private scheduleRefresh(payload: ProjectEntitlementPayload): void {
    if (this.refreshTimer) clearTimeout(this.refreshTimer);
    const delay = Math.max(1_000, payload.refreshAfter * 1000 - Date.now());
    const maxDelay = 2_147_000_000;
    this.refreshTimer = setTimeout(() => {
      this.refreshTimer = undefined;
      if (delay > maxDelay) {
        this.scheduleRefresh(payload);
        return;
      }
      if (isBrowserOnline()) void this.refresh();
    }, Math.min(delay, maxDelay));
  }
}

