import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { DatabaseSync } from 'node:sqlite';
import { generateCompatibleLicenseCode, verifyCompatibleLicenseCode } from './licenseCode.js';

const DATA_DIR = process.env.DATA_DIR || path.resolve(process.cwd(), 'data');
const DB_FILE = path.join(DATA_DIR, 'db.sqlite');
const LEGACY_JSON_FILE = path.join(DATA_DIR, 'db.json');

fs.mkdirSync(DATA_DIR, { recursive: true });
const db = new DatabaseSync(DB_FILE);
db.exec(`
  PRAGMA journal_mode = WAL;
  PRAGMA synchronous = FULL;
  PRAGMA busy_timeout = 5000;
  CREATE TABLE IF NOT EXISTS customers (id TEXT PRIMARY KEY, data TEXT NOT NULL);
  CREATE TABLE IF NOT EXISTS subscriptions (id TEXT PRIMARY KEY, data TEXT NOT NULL);
  CREATE TABLE IF NOT EXISTS licenses (id TEXT PRIMARY KEY, data TEXT NOT NULL);
  CREATE TABLE IF NOT EXISTS checkout_sessions (
    id TEXT PRIMARY KEY,
    customer_id TEXT,
    email TEXT,
    created_at TEXT NOT NULL
  );
  CREATE TABLE IF NOT EXISTS webhook_events (
    id TEXT PRIMARY KEY,
    status TEXT NOT NULL,
    error TEXT,
    updated_at TEXT NOT NULL
  );
  CREATE TABLE IF NOT EXISTS processed_objects (
    kind TEXT NOT NULL,
    object_id TEXT NOT NULL,
    status TEXT NOT NULL,
    error TEXT,
    updated_at TEXT NOT NULL,
    PRIMARY KEY (kind, object_id)
  );
  CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
`);

const TABLES = new Set(['customers', 'subscriptions', 'licenses']);
const PLAN_DURATIONS = { premium_30d: 30, premium_90d: 90, premium_180d: 180, premium_365d: 365 };
const STRIPE_STATUS_RANK = { grace_expired: 0, past_due: 1, trialing: 2, active: 3, paid: 3, canceled: 4 };

export function isStripeTransitionStale(current, eventCreated = 0, eventId = '', incomingStatus = '') {
  const incomingTime = Number(eventCreated || 0);
  const previousTime = Number(current?.lastStripeEventCreated || 0);
  if (incomingTime && previousTime && incomingTime < previousTime) return true;
  if (!incomingTime || !previousTime || incomingTime > previousTime) return false;
  const incomingRank = STRIPE_STATUS_RANK[incomingStatus] ?? 0;
  const previousRank = STRIPE_STATUS_RANK[current?.status] ?? 0;
  if (incomingRank !== previousRank) return incomingRank < previousRank;
  const previousId = String(current?.lastStripeEventId || '');
  return Boolean(previousId && eventId && String(eventId) <= previousId);
}

function transaction(fn) {
  db.exec('BEGIN IMMEDIATE');
  try {
    const result = fn();
    db.exec('COMMIT');
    return result;
  } catch (error) {
    try { db.exec('ROLLBACK'); } catch (_) {}
    throw error;
  }
}

function decode(row) {
  return row ? JSON.parse(row.data) : null;
}

function loadOne(table, id) {
  if (!TABLES.has(table)) throw new Error('Invalid table');
  return decode(db.prepare(`SELECT data FROM ${table} WHERE id = ?`).get(id));
}

function loadAll(table) {
  if (!TABLES.has(table)) throw new Error('Invalid table');
  return db.prepare(`SELECT data FROM ${table}`).all().map(decode);
}

function saveOne(table, id, value) {
  if (!TABLES.has(table)) throw new Error('Invalid table');
  db.prepare(`INSERT INTO ${table} (id, data) VALUES (?, ?)
    ON CONFLICT(id) DO UPDATE SET data = excluded.data`).run(id, JSON.stringify(value));
  return value;
}

function migrateLegacyJson() {
  const count = Number(db.prepare('SELECT COUNT(*) AS count FROM licenses').get().count);
  if (count > 0 || !fs.existsSync(LEGACY_JSON_FILE)) return;
  let legacy;
  try { legacy = JSON.parse(fs.readFileSync(LEGACY_JSON_FILE, 'utf8')); } catch (_) { return; }
  transaction(() => {
    let maxCodeId = 0;
    for (const [id, value] of Object.entries(legacy.customers || {})) saveOne('customers', id, value);
    for (const [id, value] of Object.entries(legacy.subscriptions || {})) saveOne('subscriptions', id, value);
    for (const [id, value] of Object.entries(legacy.licenses || {})) {
      saveOne('licenses', id, value);
      const parsed = verifyCompatibleLicenseCode(value.key);
      if (parsed.valid) maxCodeId = Math.max(maxCodeId, parsed.codeId);
    }
    db.prepare(`INSERT OR REPLACE INTO meta (key, value) VALUES ('license_code_counter', ?)`)
      .run(String(maxCodeId));
    for (const [id, value] of Object.entries(legacy.webhookEvents || {})) {
      db.prepare(`INSERT OR REPLACE INTO webhook_events (id, status, error, updated_at) VALUES (?, ?, ?, ?)`)
        .run(id, value.status || 'completed', value.error || null, value.updatedAt || new Date().toISOString());
    }
    for (const customer of Object.values(legacy.customers || {})) {
      if (customer.checkoutSessionId) {
        db.prepare(`INSERT OR REPLACE INTO checkout_sessions (id, customer_id, email, created_at) VALUES (?, ?, ?, ?)`)
          .run(customer.checkoutSessionId, customer.id || null, customer.email || null, customer.updatedAt || new Date().toISOString());
      }
    }
  });
  fs.copyFileSync(LEGACY_JSON_FILE, `${LEGACY_JSON_FILE}.migrated-backup`);
}

migrateLegacyJson();

export function createLicenseKey(durationDays) {
  db.prepare(`INSERT OR IGNORE INTO meta (key, value) VALUES ('license_code_counter', '0')`).run();
  const row = db.prepare(`UPDATE meta SET value = CAST(value AS INTEGER) + 1
    WHERE key = 'license_code_counter' RETURNING value`).get();
  const codeId = Number(row.value);
  return { key: generateCompatibleLicenseCode(durationDays, codeId), codeId };
}

function recomputeEntitlement(license) {
  const now = Date.now();
  const oneTimeExpiry = license.oneTimeExpiresAt ? new Date(license.oneTimeExpiresAt).getTime() : 0;
  const subscriptionExpiry = license.subscriptionExpiresAt ? new Date(license.subscriptionExpiresAt).getTime() : 0;
  const graceExpiry = license.subscriptionGraceUntil ? new Date(license.subscriptionGraceUntil).getTime() : 0;
  const oneTimeActive = oneTimeExpiry > now;
  const subscriptionStatus = license.subscriptionStatus || license.status;
  const subscriptionActive = ['active', 'trialing'].includes(subscriptionStatus) && subscriptionExpiry > now;
  const subscriptionGraceActive = subscriptionStatus === 'past_due' && graceExpiry > now;
  const canceledPaidThrough = subscriptionStatus === 'canceled' && subscriptionExpiry > now;
  const active = oneTimeActive || subscriptionActive || subscriptionGraceActive || canceledPaidThrough;
  const effectiveExpiry = Math.max(
    oneTimeActive ? oneTimeExpiry : 0,
    subscriptionActive || canceledPaidThrough ? subscriptionExpiry : 0,
    subscriptionGraceActive ? graceExpiry : 0
  );
  return {
    ...license,
    active,
    status: oneTimeActive ? 'active' : (subscriptionStatus || (active ? 'active' : 'expired')),
    expiresAt: effectiveExpiry ? new Date(effectiveExpiry).toISOString() : license.expiresAt
  };
}

export function upsertCustomer(customer) {
  return transaction(() => {
    const next = { ...(loadOne('customers', customer.id) || {}), ...customer, updatedAt: new Date().toISOString() };
    saveOne('customers', customer.id, next);
    if (customer.checkoutSessionId) {
      db.prepare(`INSERT OR REPLACE INTO checkout_sessions (id, customer_id, email, created_at) VALUES (?, ?, ?, ?)`)
        .run(customer.checkoutSessionId, customer.id || null, customer.email || null, new Date().toISOString());
    }
    return next;
  });
}

export function upsertSubscription(subscription) {
  return transaction(() => {
    const existing = loadOne('subscriptions', subscription.id) || {};
    const incomingEvent = Number(subscription.lastStripeEventCreated || 0);
    const previousEvent = Number(existing.lastStripeEventCreated || 0);
    if (isStripeTransitionStale(existing, incomingEvent, subscription.lastStripeEventId, subscription.status)) {
      return { ...existing, staleEventIgnored: true };
    }
    const next = {
      ...existing,
      ...subscription,
      lastStripeEventCreated: incomingEvent ? Math.max(incomingEvent, previousEvent) : previousEvent || null,
      lastStripeEventId: subscription.lastStripeEventId || existing.lastStripeEventId || null,
      updatedAt: new Date().toISOString()
    };
    return saveOne('subscriptions', subscription.id, next);
  });
}

export function createOrUpdateLicenseBySubscription(subscriptionId, patch) {
  return transaction(() => {
    const licenses = loadAll('licenses');
    const existing = licenses.find((lic) => lic.subscriptionId === subscriptionId)
      || licenses.find((lic) => patch.customerId && lic.customerId === patch.customerId);
    const id = existing?.id || crypto.randomUUID();
    const durationDays = patch.durationDays || PLAN_DURATIONS[patch.planCode];
    const allocated = existing ? null : createLicenseKey(durationDays);
    const inferredOneTimeExpiry = existing?.oneTimeExpiresAt
      || (existing?.paymentIntentId || existing?.processedPaymentIntentIds?.length ? existing.expiresAt : null);
    const raw = {
      id,
      key: existing?.key || allocated.key,
      codeId: existing?.codeId || allocated.codeId,
      createdAt: existing?.createdAt || new Date().toISOString(),
      ...existing,
      ...patch,
      subscriptionId,
      subscriptionStatus: patch.status || existing?.subscriptionStatus,
      subscriptionExpiresAt: patch.expiresAt || existing?.subscriptionExpiresAt,
      subscriptionGraceUntil: patch.graceUntil ?? existing?.subscriptionGraceUntil ?? null,
      oneTimeExpiresAt: inferredOneTimeExpiry,
      durationDays: durationDays || existing?.durationDays,
      updatedAt: new Date().toISOString()
    };
    const next = recomputeEntitlement(raw);
    return saveOne('licenses', id, next);
  });
}

export function grantOneTimeLicenseAtomic({ paymentIntentId, customerId, customerEmail, planCode, durationDays, premiumFeatures }) {
  return transaction(() => {
    const licenses = loadAll('licenses');
    const existing = licenses.find((lic) => lic.customerId === customerId && customerId);
    if (existing) {
      const processed = Array.isArray(existing.processedPaymentIntentIds) ? existing.processedPaymentIntentIds : [];
      if (paymentIntentId && processed.includes(paymentIntentId)) {
        return { ...existing, idempotent: true, extended: false, addedDays: 0 };
      }
      const paidThrough = existing.oneTimeExpiresAt || existing.expiresAt;
      const base = paidThrough && new Date(paidThrough).getTime() > Date.now()
        ? new Date(paidThrough) : new Date();
      base.setUTCDate(base.getUTCDate() + durationDays);
      const raw = {
        ...existing,
        customerId: customerId || existing.customerId,
        customerEmail: customerEmail || existing.customerEmail,
        planCode,
        oneTimeExpiresAt: base.toISOString(),
        premiumFeatures,
        paymentIntentId: paymentIntentId || existing.paymentIntentId,
        processedPaymentIntentIds: paymentIntentId ? [...processed, paymentIntentId] : processed,
        updatedAt: new Date().toISOString()
      };
      const next = recomputeEntitlement(raw);
      saveOne('licenses', existing.id, next);
      return { ...next, extended: true, addedDays: durationDays };
    }

    const expiresAt = new Date();
    expiresAt.setUTCDate(expiresAt.getUTCDate() + durationDays);
    const id = crypto.randomUUID();
    const allocated = createLicenseKey(durationDays);
    const next = {
      id,
      key: allocated.key,
      codeId: allocated.codeId,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      subscriptionId: null,
      customerId,
      customerEmail,
      planCode,
      status: 'active',
      expiresAt: expiresAt.toISOString(),
      oneTimeExpiresAt: expiresAt.toISOString(),
      active: true,
      premiumFeatures,
      machineFingerprint: null,
      deactivatedAt: null,
      paymentIntentId,
      processedPaymentIntentIds: paymentIntentId ? [paymentIntentId] : []
    };
    return saveOne('licenses', id, next);
  });
}

export function findLicenseByKey(key) {
  return loadAll('licenses').find((lic) => lic.key === key) || null;
}

export function findActiveLicenseByCustomerEmail(email) {
  if (!email) return null;
  const normalized = email.toLowerCase();
  return loadAll('licenses').find((lic) => lic.customerEmail?.toLowerCase() === normalized && lic.active) || null;
}

export function findLicenseByCustomerId(customerId) {
  if (!customerId) return null;
  return loadAll('licenses').find((lic) => lic.customerId === customerId) || null;
}

export function findCheckoutResult(sessionId) {
  if (!sessionId) return null;
  const session = db.prepare('SELECT customer_id, email FROM checkout_sessions WHERE id = ?').get(sessionId);
  if (!session) return null;
  const license = session.customer_id
    ? loadAll('licenses').find((item) => item.customerId === session.customer_id) || null
    : null;
  return { session, license };
}

export function claimWebhookEvent(eventId) {
  if (!eventId) return { claimed: false, status: 'invalid' };
  return transaction(() => {
    const existing = db.prepare('SELECT status, updated_at FROM webhook_events WHERE id = ?').get(eventId);
    const processingFresh = existing?.status === 'processing'
      && Date.now() - new Date(existing.updated_at).getTime() < 5 * 60 * 1000;
    if (existing?.status === 'completed') return { claimed: false, status: 'completed' };
    if (processingFresh) return { claimed: false, status: 'processing' };
    db.prepare(`INSERT INTO webhook_events (id, status, error, updated_at) VALUES (?, 'processing', NULL, ?)
      ON CONFLICT(id) DO UPDATE SET status='processing', error=NULL, updated_at=excluded.updated_at`)
      .run(eventId, new Date().toISOString());
    return { claimed: true, status: 'processing' };
  });
}

export function finishWebhookEvent(eventId, error = null) {
  if (!eventId) return;
  transaction(() => {
    db.prepare(`INSERT INTO webhook_events (id, status, error, updated_at) VALUES (?, ?, ?, ?)
      ON CONFLICT(id) DO UPDATE SET status=excluded.status, error=excluded.error, updated_at=excluded.updated_at`)
      .run(eventId, error ? 'failed' : 'completed', error ? String(error).slice(0, 500) : null, new Date().toISOString());
  });
}

export function claimBusinessObject(kind, objectId) {
  if (!kind || !objectId) return { claimed: false, status: 'invalid' };
  return transaction(() => {
    const existing = db.prepare('SELECT status, updated_at FROM processed_objects WHERE kind = ? AND object_id = ?')
      .get(kind, objectId);
    const processingFresh = existing?.status === 'processing'
      && Date.now() - new Date(existing.updated_at).getTime() < 5 * 60 * 1000;
    if (existing?.status === 'completed') return { claimed: false, status: 'completed' };
    if (processingFresh) return { claimed: false, status: 'processing' };
    db.prepare(`INSERT INTO processed_objects (kind, object_id, status, error, updated_at)
      VALUES (?, ?, 'processing', NULL, ?)
      ON CONFLICT(kind, object_id) DO UPDATE SET status='processing', error=NULL, updated_at=excluded.updated_at`)
      .run(kind, objectId, new Date().toISOString());
    return { claimed: true, status: 'processing' };
  });
}

export function finishBusinessObject(kind, objectId, error = null) {
  if (!kind || !objectId) return;
  transaction(() => {
    db.prepare(`INSERT INTO processed_objects (kind, object_id, status, error, updated_at)
      VALUES (?, ?, ?, ?, ?)
      ON CONFLICT(kind, object_id) DO UPDATE SET status=excluded.status, error=excluded.error, updated_at=excluded.updated_at`)
      .run(kind, objectId, error ? 'failed' : 'completed', error ? String(error).slice(0, 500) : null, new Date().toISOString());
  });
}

export function cancelLicenseBySubscription(subscriptionId, eventCreated = 0, eventId = '') {
  if (!subscriptionId) return null;
  return transaction(() => {
    const license = loadAll('licenses').find((item) => item.subscriptionId === subscriptionId);
    if (!license) return null;
    const incomingEvent = Number(eventCreated || 0);
    const previousEvent = Number(license.lastStripeEventCreated || 0);
    if (isStripeTransitionStale({ ...license, status: license.subscriptionStatus || license.status }, incomingEvent, eventId, 'canceled')) {
      return { ...license, staleEventIgnored: true };
    }
    const raw = {
      ...license,
      subscriptionStatus: 'canceled',
      subscriptionExpiresAt: license.subscriptionExpiresAt || license.expiresAt,
      lastStripeEventCreated: incomingEvent ? Math.max(incomingEvent, previousEvent) : previousEvent || null,
      lastStripeEventId: eventId || license.lastStripeEventId || null,
      updatedAt: new Date().toISOString()
    };
    return saveOne('licenses', license.id, recomputeEntitlement(raw));
  });
}

export function markLicensePaymentFailed(subscriptionId, graceUntil, eventCreated = 0, eventId = '') {
  if (!subscriptionId) return null;
  return transaction(() => {
    const license = loadAll('licenses').find((item) => item.subscriptionId === subscriptionId);
    if (!license) return null;
    const incomingEvent = Number(eventCreated || 0);
    const previousEvent = Number(license.lastStripeEventCreated || 0);
    if (isStripeTransitionStale({ ...license, status: license.subscriptionStatus || license.status }, incomingEvent, eventId, 'past_due')) {
      return { ...license, staleEventIgnored: true };
    }
    const currentGrace = license.subscriptionGraceUntil || license.graceUntil;
    const effectiveGraceUntil = currentGrace
      && new Date(currentGrace).getTime() > new Date(graceUntil).getTime()
      ? currentGrace : graceUntil;
    const raw = {
      ...license,
      subscriptionStatus: 'past_due',
      subscriptionGraceUntil: effectiveGraceUntil,
      graceUntil: effectiveGraceUntil,
      lastStripeEventCreated: incomingEvent ? Math.max(incomingEvent, previousEvent) : previousEvent || null,
      lastStripeEventId: eventId || license.lastStripeEventId || null,
      updatedAt: new Date().toISOString()
    };
    return saveOne('licenses', license.id, recomputeEntitlement(raw));
  });
}

export function extendLicenseByDuration(licenseId, days) {
  return transaction(() => {
    const lic = loadOne('licenses', licenseId);
    if (!lic) return null;
    const base = lic.expiresAt && new Date(lic.expiresAt).getTime() > Date.now() ? new Date(lic.expiresAt) : new Date();
    base.setUTCDate(base.getUTCDate() + days);
    return saveOne('licenses', licenseId, {
      ...lic,
      expiresAt: base.toISOString(),
      active: true,
      activatedAt: lic.activatedAt || new Date().toISOString(),
      updatedAt: new Date().toISOString()
    });
  });
}

export function updateLicense(id, patch) {
  return transaction(() => {
    const existing = loadOne('licenses', id);
    if (!existing) return null;
    return saveOne('licenses', id, { ...existing, ...patch, updatedAt: new Date().toISOString() });
  });
}

export function claimLicenseMachine(key, fingerprint) {
  return transaction(() => {
    const license = loadAll('licenses').find((item) => item.key === key);
    if (!license) return { claimed: false, reason: 'not_found', license: null };
    if (license.machineFingerprint && license.machineFingerprint !== fingerprint) {
      return { claimed: false, reason: 'different_machine', license };
    }
    if (license.machineFingerprint === fingerprint) {
      return { claimed: true, existing: true, license };
    }
    const next = {
      ...license,
      machineFingerprint: fingerprint,
      activatedAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };
    saveOne('licenses', license.id, next);
    return { claimed: true, existing: false, license: next };
  });
}

export function getStoreSnapshot() {
  const toMap = (items) => Object.fromEntries(items.map((item) => [item.id, item]));
  const webhookEvents = Object.fromEntries(db.prepare('SELECT id, status, error, updated_at FROM webhook_events').all()
    .map((item) => [item.id, { status: item.status, error: item.error, updatedAt: item.updated_at }]));
  return {
    customers: toMap(loadAll('customers')),
    subscriptions: toMap(loadAll('subscriptions')),
    licenses: toMap(loadAll('licenses')),
    webhookEvents
  };
}
