const crypto = require('crypto');
const Database = require('better-sqlite3');
const path = require('path');
const LicenseCache = require('../services/LicenseCacheService');
const SupabaseLicenseService = require('../services/SupabaseLicenseService');

const SUPERADMIN_PUBLIC_KEY = (process.env.SUPERADMIN_PUBLIC_KEY || '').replace(/\\n/g, '\n');
const DEFAULT_TENANT_ID = '5b39bcb5-506a-456d-b7cb-91730b397cc1';
const CLOCK_DRIFT_THRESHOLD_MS = 300000;
const READ_ONLY_METHODS = ['GET', 'HEAD', 'OPTIONS'];
const BYPASS_PREFIXES = [
  '/api/auth/login',
  '/api/auth/refresh',
  '/api/auth/refresh-roles',        // Phase 1 PR 1: auxiliary-role refresh endpoint
  '/api/user-roles',                  // Phase 1 PR 1: admin-only user_roles controller
  '/api/users/',                       // /api/users/:id/roles is mounted under /api/users
  '/api/sysadmin',
  '/api/health',
  '/api/billing',
  '/api/v1/auth/jwks',
  '/api/license/current',
  '/metrics',
  '/api/metrics'
];

let lastSuperAdminTimeSync = Date.now();
let serverTimeDriftOffsetMs = 0;

function updateSystemClockSync(superAdminTimestamp) {
  if (superAdminTimestamp) {
    const remoteTime = new Date(superAdminTimestamp).getTime();
    if (Number.isFinite(remoteTime)) {
      serverTimeDriftOffsetMs = Math.abs(Date.now() - remoteTime);
      lastSuperAdminTimeSync = Date.now();
    }
  }
}

function verifyEdDsaJwt(token, publicKey) {
  if (!publicKey) return { isValid: false, error: 'Missing public key' };
  const segments = String(token || '').split('.');
  if (segments.length !== 3) return { isValid: false, error: 'Invalid token format' };
  const [headerB64, payloadB64, signatureB64] = segments;
  let header;
  let payload;
  try {
    header = JSON.parse(Buffer.from(headerB64, 'base64url').toString('utf8'));
    payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8'));
  } catch (_) {
    return { isValid: false, error: 'Invalid token JSON' };
  }
  if (!header || header.alg !== 'EdDSA') return { isValid: false, error: 'Unsupported algorithm' };
  let ok = false;
  try {
    ok = crypto.verify(
      null,
      Buffer.from(`${headerB64}.${payloadB64}`, 'utf8'),
      publicKey,
      Buffer.from(signatureB64, 'base64url')
    );
  } catch (_) {
    return { isValid: false, error: 'Signature verification threw' };
  }
  if (!ok) return { isValid: false, error: 'Invalid signature' };
  return { isValid: true, payload };
}

function isBypassPath(req) {
  if (req.method === 'OPTIONS') return true;
  return BYPASS_PREFIXES.some((p) => req.path.startsWith(p));
}

function isReadOnlyMethod(req) {
  return READ_ONLY_METHODS.includes(String(req.method || '').toUpperCase());
}

function rejectMutation(res, code, message) {
  return res.status(402).json({
    error: 'Payment Required / Term License Expired',
    code,
    message,
    billing_url: '/billing'
  });
}

function resolveTenantId(req) {
  return (
    req.tenantId ||
    (req.headers && (req.headers['x-tenant-id'] || req.headers['x-tenant-code'])) ||
    process.env.TENANT_ID ||
    DEFAULT_TENANT_ID
  );
}

function readLocalLicenseRow(tenantId) {
  const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/school.db');
  let db = null;
  let row = null;
  try {
    db = new Database(dbPath);
    db.pragma('foreign_keys = ON');
    row = db
      .prepare(
        "SELECT id, tenant_id, license_key, signed_license_token, algorithm, term_name, academic_year, start_date, end_date, grace_period_days, max_students, max_staff, status, override_reason, issued_by FROM tenant_licenses WHERE tenant_id = ? ORDER BY date(end_date) DESC, rowid DESC LIMIT 1"
      )
      .get(tenantId);
  } catch (_) {
    row = null;
  } finally {
    if (db) {
      try { db.close(); } catch (_) {}
    }
  }
  return row;
}

function rowToCachedPayload(row) {
  if (!row || !row.signed_license_token) return null;
  const token = row.signed_license_token;
  const segments = String(token).split('.');
  if (segments.length !== 3) return null;
  try {
    const payload = JSON.parse(Buffer.from(segments[1], 'base64url').toString('utf8'));
    // Carry the source row alongside the JWT payload so middleware can
    // distinguish a Supabase-fresh row from a stale local cache.
    payload._source_row = row;
    return payload;
  } catch (_) {
    return null;
  }
}

async function verifyLicenseMiddleware(req, res, next) {
  try {
    return await verifyLicenseMiddlewareInner(req, res, next);
  } catch (err) {
    console.error('Licensing Middleware Evaluation Error:', err);
    if (isReadOnlyMethod(req)) {
      res.setHeader('X-License-Status', 'Evaluation-Error');
      return next();
    }
    return rejectMutation(res, 'EVALUATION_ERROR', err && err.message ? err.message : 'License evaluation failed.');
  }
}

function getSuperAdminPublicKey() {
  return (process.env.SUPERADMIN_PUBLIC_KEY || '').replace(/\\n/g, '\n');
}

async function verifyLicenseMiddlewareInner(req, res, next) {
  const tenantId = resolveTenantId(req);
  req.tenantId = tenantId;

  if (isBypassPath(req) || req.user?.role === 'systems_admin') {
    return next();
  }

  if (process.env.NODE_ENV === 'test' && process.env.ENABLE_LICENSING_IN_TESTS !== 'true') {
    res.setHeader('X-License-Status', 'Test-Bypass');
    return next();
  }

  const superAdminPublicKey = getSuperAdminPublicKey();

  // Dev mode: no superadmin public key configured means the licensing
  // system isn't wired up. Don't block mutations — let the dev work.
  // In production, SUPERADMIN_PUBLIC_KEY must be set, in which case
  // this branch is skipped and the full EdDSA verification runs.
  if (!superAdminPublicKey) {
    res.setHeader('X-License-Status', 'Dev-Bypass');
    return next();
  }

  if (serverTimeDriftOffsetMs > CLOCK_DRIFT_THRESHOLD_MS) {
    if (isReadOnlyMethod(req)) {
      res.setHeader('X-License-Status', 'Clock-Drift-Restricted');
      return next();
    }
    return rejectMutation(res, 'CLOCK_DRIFT_DETECTED', 'System clock drift detected.');
  }

  let cachedPayload = null;
  try {
    cachedPayload = await LicenseCache.get(tenantId);
  } catch (_) {
    cachedPayload = null;
  }

  // Track where the active license came from so the response headers
  // and the UI can surface the source. `cache` here means the LRU/Redis
  // cache; `supabase` is a fresh Supabase fetch; `local` is the last-resort
  // fallback to the tenant's own SQLite.
  let licenseSource = cachedPayload ? 'cache' : null;

  if (!cachedPayload) {
    // Try Supabase first — the SuperAdmin publishes the signed license
    // there at issuance time. This is the cloud source of truth.
    let supabaseRow = null;
    let supabaseErr = null;
    if (SupabaseLicenseService.isConfigured()) {
      try {
        supabaseRow = await SupabaseLicenseService.fetchActiveLicense(tenantId);
      } catch (err) {
        supabaseErr = err;
        console.warn(
          `[licensing] Supabase fetch failed for tenant ${tenantId}: ${err.message}`
        );
      }
    }

    if (supabaseRow && supabaseRow.signed_license_token) {
      // Verify the EdDSA signature against the SUPERADMIN public key.
      const verification = verifyEdDsaJwt(supabaseRow.signed_license_token, superAdminPublicKey);
      if (!verification || !verification.isValid) {
        if (isReadOnlyMethod(req)) {
          res.setHeader('X-License-Status', 'Invalid-Signature');
          return next();
        }
        return rejectMutation(res, 'INVALID_SIGNATURE', 'License signature verification failed.');
      }

      // Write-back to local SQLite for offline resilience. If Supabase
      // is unreachable on the next request, the middleware falls back to
      // this row.
      try {
        SupabaseLicenseService.upsertLicenseCache(supabaseRow);
      } catch (_) {}

      cachedPayload = verification.payload;
      cachedPayload._source_row = supabaseRow;
      licenseSource = 'supabase';

      const nowSec = Math.floor(Date.now() / 1000);
      const remainingTTL = Math.max((cachedPayload.exp || 0) - nowSec, 60);
      try {
        await LicenseCache.set(tenantId, cachedPayload, remainingTTL);
      } catch (_) {}
    } else {
      // Supabase miss or error — fall back to local SQLite.
      const localRow = readLocalLicenseRow(tenantId);
      if (!localRow || !localRow.signed_license_token) {
        if (isReadOnlyMethod(req)) {
          res.setHeader('X-License-Status', 'No-License');
          res.setHeader('X-License-Source', supabaseErr ? 'supabase-error' : 'none');
          return next();
        }
        return rejectMutation(res, 'NO_LICENSE', 'No active license found for tenant.');
      }

      const verification = verifyEdDsaJwt(localRow.signed_license_token, superAdminPublicKey);
      if (!verification || !verification.isValid) {
        if (isReadOnlyMethod(req)) {
          res.setHeader('X-License-Status', 'Invalid-Signature');
          return next();
        }
        return rejectMutation(res, 'INVALID_SIGNATURE', 'License signature verification failed.');
      }

      cachedPayload = verification.payload;
      cachedPayload._source_row = localRow;
      licenseSource = 'local';
    }
  }

  const currentSeconds = Math.floor(Date.now() / 1000);
  if (currentSeconds < (cachedPayload.nbf || 0)) {
    if (isReadOnlyMethod(req)) {
      res.setHeader('X-License-Status', 'Not-Yet-Valid');
      res.setHeader('X-License-Source', licenseSource);
      return next();
    }
    return rejectMutation(res, 'NOT_YET_VALID', 'License not yet valid.');
  }
  if (currentSeconds > (cachedPayload.exp || 0)) {
    if (isReadOnlyMethod(req)) {
      res.setHeader('X-License-Status', 'Expired');
      res.setHeader('X-License-Source', licenseSource);
      return next();
    }
    return rejectMutation(res, 'LICENSE_EXPIRED', 'Your termly license has expired.');
  }

  res.setHeader('X-License-Status', 'ACTIVE');
  res.setHeader('X-License-Source', licenseSource);
  req.license = { tenantId, payload: cachedPayload, source: licenseSource };
  return next();
}

module.exports = verifyLicenseMiddleware;
module.exports.updateSystemClockSync = updateSystemClockSync;
module.exports._state = () => ({
  serverTimeDriftOffsetMs,
  lastSuperAdminTimeSync
});
module.exports._rowToCachedPayload = rowToCachedPayload;
module.exports._readLocalLicenseRow = readLocalLicenseRow;
