/**
 * DashCaddy License Manager
 *
 * Runtime license validation, activation, and feature gating.
 * Uses credential-manager for secure storage of activation tokens.
 *
 * Hybrid model:
 * - First activation: online validation against license server (if reachable)
 * - Fallback: offline HMAC validation using embedded master secret hash
 * - Ongoing: locally stored activation token checked on each premium request
 */

const crypto = require('crypto');
const os = require('os');
const fs = require('fs');
const path = require('path');
const { verifyCode, parseCode, VALID_DURATIONS } = require('./license-keygen');
const { errorResponse } = require('../utils/responses');

const LICENSE_CRED_KEY = 'license.activation';
const LICENSE_SERVER_URL = process.env.LICENSE_SERVER_URL || null; // Set when license server exists

// Features gated behind premium
const PREMIUM_FEATURES = {
  sso: { name: 'Auto-Login SSO', description: 'Automatic single sign-on for deployed apps' },
  recipes: { name: 'Recipes', description: 'Multi-container stack deployment' },
  swarm: { name: 'Docker Swarm', description: 'Multi-node cluster orchestration' }
};

class LicenseManager {
  constructor(credentialManager, configFile, log) {
    this.credentialManager = credentialManager;
    this.configFile = configFile;
    this.log = log || console;
    this.activation = null; // Cached activation state
    this.masterSecretHash = null; // Loaded from shipped secret hash (not the secret itself)
    this._loaded = false;
  }

  /**
   * Load license state from storage on startup.
   * Primary: encrypted credential store. Fallback: config.json backup.
   * If the credential store fails (e.g. encryption key changed after rebuild),
   * restores from the config.json backup automatically.
   */
  async load() {
    try {
      const stored = await this.credentialManager.retrieve(LICENSE_CRED_KEY);
      if (stored) {
        this.activation = JSON.parse(stored);
        if (this.isExpired()) {
          this.log.info?.('license', 'License has expired', {
            code: this._maskCode(this.activation.code),
            expiredAt: this.activation.expiresAt
          });
        } else {
          this.log.info?.('license', 'License loaded', {
            code: this._maskCode(this.activation.code),
            expiresAt: this.activation.expiresAt,
            daysRemaining: this.daysRemaining()
          });
        }
        this._loaded = true;
        return;
      }
    } catch (error) {
      this.log.warn?.('license', 'Failed to load from credential store, trying config backup', { error: error.message });
    }

    // Fallback: restore from config.json backup
    try {
      const fsp = require('fs').promises;
      const data = await fsp.readFile(this.configFile, 'utf8');
      const config = JSON.parse(data);
      if (config.licenseBackup) {
        this.activation = config.licenseBackup;
        this.log.info?.('license', 'License restored from config backup', {
          code: this._maskCode(this.activation.code),
          lifetime: this.activation.lifetime
        });
        // Re-store in credential manager so future loads succeed
        try {
          await this.credentialManager.store(LICENSE_CRED_KEY, JSON.stringify(this.activation));
          this.log.info?.('license', 'License re-stored in credential manager');
        } catch (storeErr) {
          this.log.warn?.('license', 'Could not re-store license in credential manager', { error: storeErr.message });
        }
        this._loaded = true;
        return;
      }
    } catch (_) {
      // Config doesn't exist or no backup — continue
    }

    this.log.info?.('license', 'No active license');
    this.activation = null;
    this._loaded = true;
  }

  /**
   * Load the shipped master secret hash for offline validation.
   * The actual master secret is NEVER shipped — only a hash of it is embedded
   * in the product, and the keygen embeds HMAC signatures in codes using the real secret.
   * For offline validation, we verify the code's internal HMAC consistency.
   *
   * @param {string} secretFile - Path to .license-secret file (dev only) or .license-secret-hash (shipped)
   */
  loadSecret(secretFile) {
    try {
      if (fs.existsSync(secretFile)) {
        const secret = fs.readFileSync(secretFile, 'utf8').trim();
        this.masterSecretHash = secret;
        return true;
      }
    } catch (error) {
      this.log.warn?.('license', 'Could not load license secret', { error: error.message });
    }
    return false;
  }

  /**
   * Generate a machine fingerprint for activation binding
   */
  getMachineFingerprint() {
    const components = [
      os.hostname(),
      os.platform(),
      os.arch(),
      os.cpus()[0]?.model || 'unknown'
    ];
    // Get primary MAC address
    const interfaces = os.networkInterfaces();
    for (const name of Object.keys(interfaces)) {
      for (const iface of interfaces[name]) {
        if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {
          components.push(iface.mac);
          break;
        }
      }
    }
    return crypto.createHash('sha256').update(components.join('|')).digest('hex').substring(0, 16);
  }

  /**
   * Activate a license code
   * @param {string} code - License code (DC-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)
   * @returns {Object} { success, message, activation? }
   */
  async activate(code) {
    if (!code || typeof code !== 'string') {
      return { success: false, message: 'License code is required' };
    }

    // Normalize code format
    code = code.trim().toUpperCase();
    if (!code.startsWith('DC-')) {
      return { success: false, message: 'Invalid code format. Codes start with DC-' };
    }

    // Check if already activated with this code
    if (this.activation && this.activation.code === code && !this.isExpired()) {
      return {
        success: true,
        message: 'This code is already activated',
        activation: this.getStatus()
      };
    }

    // Try online validation first
    let onlineResult = null;
    if (LICENSE_SERVER_URL) {
      onlineResult = await this._validateOnline(code);
      if (onlineResult && !onlineResult.success) {
        // Server explicitly rejected — don't fallback to offline
        return onlineResult;
      }
    }

    // Offline validation (HMAC check)
    if (!onlineResult) {
      const offlineResult = this._validateOffline(code);
      if (!offlineResult.valid) {
        return { success: false, message: offlineResult.reason || 'Invalid license code' };
      }

      // Code is cryptographically valid
      const machineId = this.getMachineFingerprint();
      const now = new Date();
      const isLifetime = offlineResult.durationDays === 0;
      const expiresAt = isLifetime
        ? new Date('2099-12-31T23:59:59.999Z')
        : new Date(now.getTime() + offlineResult.durationDays * 86400000);

      this.activation = {
        code,
        codeId: offlineResult.codeId,
        durationDays: offlineResult.durationDays,
        lifetime: isLifetime,
        activatedAt: now.toISOString(),
        expiresAt: expiresAt.toISOString(),
        machineId,
        validationMethod: 'offline',
        features: Object.keys(PREMIUM_FEATURES)
      };
    } else {
      // Online validation succeeded — use server response
      this.activation = onlineResult.activation;
      this.activation.validationMethod = 'online';
    }

    // Store activation token
    try {
      await this.credentialManager.store(LICENSE_CRED_KEY, JSON.stringify(this.activation), {
        activatedAt: this.activation.activatedAt,
        expiresAt: this.activation.expiresAt
      });
    } catch (error) {
      this.log.error?.('license', 'Failed to store activation', { error: error.message });
      return { success: false, message: 'License validated but failed to save activation' };
    }

    // Update config.json with license info (non-sensitive)
    await this._updateConfig();

    this.log.info?.('license', 'License activated', {
      code: this._maskCode(code),
      durationDays: this.activation.durationDays,
      expiresAt: this.activation.expiresAt,
      method: this.activation.validationMethod
    });

    const durationLabel = this.activation.lifetime ? 'lifetime' : `${this.activation.durationDays} days`;
    return {
      success: true,
      message: `License activated for ${durationLabel}`,
      activation: this.getStatus()
    };
  }

  /**
   * Deactivate the current license
   * @returns {Object} { success, message }
   */
  async deactivate() {
    if (!this.activation) {
      return { success: false, message: 'No active license to deactivate' };
    }

    const code = this._maskCode(this.activation.code);

    // If online server exists, notify it of deactivation
    if (LICENSE_SERVER_URL) {
      try {
        await this._notifyDeactivation();
      } catch (error) {
        this.log.warn?.('license', 'Could not notify license server of deactivation', { error: error.message });
      }
    }

    // Clear local activation
    await this.credentialManager.delete(LICENSE_CRED_KEY);
    this.activation = null;
    await this._updateConfig();

    this.log.info?.('license', 'License deactivated', { code });

    return { success: true, message: 'License deactivated. You can reuse this code on another machine.' };
  }

  /**
   * Get current license status
   * @returns {Object} Status object
   */
  getStatus() {
    if (!this.activation) {
      return {
        active: false,
        tier: 'free',
        features: [],
        premiumFeatures: PREMIUM_FEATURES
      };
    }

    const expired = this.isExpired();
    const isLifetime = !!(this.activation.lifetime || this.activation.durationDays === 0);
    const daysRemaining = isLifetime ? null : this.daysRemaining();

    return {
      active: !expired,
      tier: expired ? 'free' : 'premium',
      lifetime: isLifetime,
      code: this._maskCode(this.activation.code),
      durationDays: this.activation.durationDays,
      activatedAt: this.activation.activatedAt,
      expiresAt: isLifetime ? null : this.activation.expiresAt,
      daysRemaining: isLifetime ? null : Math.max(0, daysRemaining),
      expired,
      features: expired ? [] : (this.activation.features || Object.keys(PREMIUM_FEATURES)),
      premiumFeatures: PREMIUM_FEATURES,
      validationMethod: this.activation.validationMethod
    };
  }

  /**
   * Check if a specific premium feature is available
   * @param {string} feature - Feature key (e.g., 'sso', 'recipes', 'swarm')
   * @returns {boolean}
   */
  hasFeature(feature) {
    if (!this.activation) return false;
    if (this.isExpired()) return false;
    const features = this.activation.features || Object.keys(PREMIUM_FEATURES);
    return features.includes(feature);
  }

  /**
   * Check if the license has expired
   */
  isExpired() {
    if (!this.activation) return true;
    // Lifetime licenses never expire
    if (this.activation.lifetime || this.activation.durationDays === 0) return false;
    if (!this.activation.expiresAt) return false; // No expiry set = lifetime
    return Date.now() > new Date(this.activation.expiresAt).getTime();
  }

  /**
   * Get days remaining on the license
   */
  daysRemaining() {
    if (!this.activation) return 0;
    const remaining = new Date(this.activation.expiresAt).getTime() - Date.now();
    return Math.ceil(remaining / 86400000);
  }

  /**
   * Express middleware: gate a route behind a premium feature
   * @param {string} feature - Feature key
   * @returns {Function} Express middleware
   */
  requirePremium(feature) {
    return (req, res, next) => {
      if (this.hasFeature(feature)) {
        return next();
      }

      const featureInfo = PREMIUM_FEATURES[feature] || { name: feature };
      return errorResponse(res, 403, `${featureInfo.name} requires a DashCaddy Premium subscription.`, {
        premiumRequired: true,
        feature,
        featureName: featureInfo.name,
        featureDescription: featureInfo.description,
        currentTier: this.isExpired() ? 'free' : 'expired',
        upgradeUrl: '/settings#license'
      });
    };
  }

  // Private methods

  /**
   * Validate code offline using HMAC
   */
  _validateOffline(code) {
    if (!this.masterSecretHash) {
      // No secret available — try structural validation only
      try {
        const parsed = parseCode(code);
        // Without the secret we can't verify HMAC, but we can check structure
        if (parsed.version !== 1) return { valid: false, reason: 'Unsupported code version' };
        if (parsed.durationDays !== 0 && !VALID_DURATIONS.includes(parsed.durationDays)) return { valid: false, reason: 'Invalid duration' };
        // Can't verify signature without secret — reject
        return { valid: false, reason: 'License validation unavailable. Please try again when connected to the internet.' };
      } catch (e) {
        return { valid: false, reason: e.message };
      }
    }

    // Full verification with secret
    return verifyCode(this.masterSecretHash, code);
  }

  /**
   * Validate code against online license server
   */
  async _validateOnline(code) {
    try {
      const machineId = this.getMachineFingerprint();
      const response = await fetch(`${LICENSE_SERVER_URL}/api/license/validate`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ code, machineId }),
        signal: AbortSignal.timeout(10000) // 10s timeout
      });

      if (!response.ok) {
        const data = await response.json().catch(() => ({}));
        return { success: false, message: data.error || `Server returned ${response.status}` };
      }

      const data = await response.json();
      if (data.success) {
        return {
          success: true,
          activation: {
            code,
            codeId: data.codeId,
            durationDays: data.durationDays,
            activatedAt: new Date().toISOString(),
            expiresAt: data.expiresAt,
            machineId,
            features: data.features || Object.keys(PREMIUM_FEATURES),
            serverToken: data.token
          }
        };
      }

      return { success: false, message: data.message || 'License server rejected the code' };
    } catch (error) {
      // Server unreachable — return null to fallback to offline
      this.log.warn?.('license', 'License server unreachable, falling back to offline validation', {
        error: error.message
      });
      return null;
    }
  }

  /**
   * Notify license server of deactivation
   */
  async _notifyDeactivation() {
    if (!LICENSE_SERVER_URL || !this.activation) return;
    await fetch(`${LICENSE_SERVER_URL}/api/license/deactivate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        code: this.activation.code,
        machineId: this.activation.machineId,
        serverToken: this.activation.serverToken
      }),
      signal: AbortSignal.timeout(10000)
    });
  }

  /**
   * Update config.json with license info and full activation backup.
   * The backup ensures the license survives encryption key changes
   * (e.g. container rebuilds that generate new keys).
   */
  async _updateConfig() {
    try {
      const fsp = require('fs').promises;
      let config = {};
      try {
        const data = await fsp.readFile(this.configFile, 'utf8');
        config = JSON.parse(data);
      } catch (e) {
        // Config doesn't exist yet
      }

      if (this.activation && !this.isExpired()) {
        config.license = {
          active: true,
          tier: 'premium',
          expiresAt: this.activation.expiresAt,
          daysRemaining: this.daysRemaining(),
          features: this.activation.features || Object.keys(PREMIUM_FEATURES)
        };
        // Full backup of activation data (config.json is volume-mounted and persists)
        config.licenseBackup = this.activation;
      } else {
        config.license = { active: false, tier: 'free' };
        delete config.licenseBackup;
      }

      config.updatedAt = new Date().toISOString();
      await fsp.writeFile(this.configFile, JSON.stringify(config, null, 2), 'utf8');
    } catch (error) {
      this.log.error?.('license', 'Failed to update config with license info', { error: error.message });
    }
  }

  /**
   * Mask a license code for display (show first and last groups only)
   */
  _maskCode(code) {
    if (!code) return 'none';
    const parts = code.split('-');
    if (parts.length < 4) return 'DC-*****';
    return `${parts[0]}-${parts[1]}-*****-*****-${parts[parts.length - 1]}`;
  }
}

module.exports = { LicenseManager, PREMIUM_FEATURES };
