import crypto from 'crypto';
import ConfigSetting from '../models/ConfigSetting.js';
import ConfigAuditLog from '../models/ConfigAuditLog.js';
import redisClient from '../config/redis.js';

// Configuration definitions
const CONFIG_SCHEMAS = {
  // System level defaults
  'figma.api_timeout': { type: 'number', min: 1000, max: 60000 },
  'storybook.default_port': { type: 'number', min: 1, max: 65535 },
  'rate_limit.requests_per_minute': { type: 'number', min: 1, max: 1000 },

  // Project level
  'figma.file_url': { type: 'string', pattern: /^https:\/\/www\.figma\.com\/file\// },
  'figma.api_token': { type: 'string', encrypted: true, minLength: 10 },
  'storybook.base_url': { type: 'string', pattern: /^https?:\/\// },
  'jira.project_key': { type: 'string', pattern: /^[A-Z0-9]{2,10}$/ },
  'component.directory': { type: 'string', pattern: /^[a-zA-Z0-9/_-]+$/ },

  // User level
  'theme': { type: 'string', enum: ['light', 'dark', 'auto'] },
  'dashboard.layout': { type: 'string', enum: ['grid', 'list', 'compact'] },
  'notifications.enabled': { type: 'boolean' }
};

// Validate encryption key is set (32 bytes required for AES-256)
const ENCRYPTION_KEY = process.env.CONFIG_ENCRYPTION_KEY;
if (!ENCRYPTION_KEY) {
  throw new Error('FATAL: CONFIG_ENCRYPTION_KEY environment variable is required for secure configuration storage');
}
if (ENCRYPTION_KEY.length !== 32) {
  throw new Error(`FATAL: CONFIG_ENCRYPTION_KEY must be exactly 32 bytes for AES-256 (got ${ENCRYPTION_KEY.length})`);
}

const ALGORITHM = 'aes-256-gcm';
const CACHE_TTL_SECONDS = 300; // 5 minutes

class ConfigService {
  /**
   * Resolve configuration with cascade strategy: System -> Project -> User
   * @param {string} projectId - Optional project context
   * @param {string} userId - Optional user context
   * @returns {Promise<Object>} Resolved configuration object
   */
  async resolveConfig(projectId = null, userId = null) {
    const cacheKey = `config:resolved:${projectId || 'none'}:${userId || 'none'}`;

    // 1. Try Cache
    if (redisClient && redisClient.status === 'ready') {
      try {
        const cached = await redisClient.get(cacheKey);
        if (cached) return JSON.parse(cached);
      } catch (err) {
        console.warn('[ConfigService] Redis read failed', err);
      }
    }

    // 2. Fetch all relevant scopes in parallel
    const queries = [
      ConfigSetting.findAll({ where: { scope: 'SYSTEM' } })
    ];

    if (projectId) {
      queries.push(ConfigSetting.findAll({ where: { scope: 'PROJECT', scopeId: projectId } }));
    }

    if (userId) {
      queries.push(ConfigSetting.findAll({ where: { scope: 'USER', scopeId: userId } }));
    }

    const results = await Promise.all(queries);
    const systemConfig = this._processConfigList(results[0]);
    const projectConfig = projectId ? this._processConfigList(results[1]) : {};
    const userConfig = userId ? this._processConfigList(results[projectId ? 2 : 1]) : {};

    // 3. Deep Merge: System <- Project <- User
    // Note: Simple object merge is sufficient here as keys are dot-notation strings in DB
    // but represented as flat objects during resolution for keys.
    // If we want nested objects returned, we would hydrate them here.
    // For now, returning flat key-value map is safer and standard for config flags.

    const resolved = {
      ...systemConfig,
      ...projectConfig,
      ...userConfig
    };

    // 4. Cache Result
    if (redisClient && redisClient.status === 'ready') {
      try {
        await redisClient.set(cacheKey, JSON.stringify(resolved), 'EX', CACHE_TTL_SECONDS);
      } catch (err) {
        console.warn('[ConfigService] Redis set failed', err);
      }
    }

    return resolved;
  }

  /**
   * Set a configuration value
   * @param {Object} params
   * @param {string} params.scope - SYSTEM, PROJECT, or USER
   * @param {string} params.scopeId - UUID or null for SYSTEM
   * @param {string} params.key - Config key
   * @param {any} params.value - Value to store
   * @param {string} params.actorId - ID of user making change
   */
  async setConfig({ scope, scopeId, key, value, actorId }) {
    // 1. Validate Key & Schema
    const schema = CONFIG_SCHEMAS[key];
    if (!schema) {
      throw new Error(`Invalid config key: ${key}`);
    }
    this._validateValue(value, schema);

    // 2. Encrypt if needed
    let finalValue = value;
    const isSecret = schema.encrypted || false;

    if (isSecret) {
      finalValue = this._encrypt(value);
    }

    // 3. Upsert to DB
    const existing = await ConfigSetting.findOne({
      where: { scope, scopeId, key }
    });

    let record;
    if (existing) {
      // Create Audit Log
      await ConfigAuditLog.create({
        configId: existing.id,
        actorId,
        action: 'UPDATE',
        previousValue: existing.value,
        newValue: finalValue
      });

      existing.value = finalValue;
      existing.isSecret = isSecret;
      await existing.save();
      record = existing;
    } else {
      record = await ConfigSetting.create({
        scope,
        scopeId,
        key,
        value: finalValue,
        isSecret
      });

      await ConfigAuditLog.create({
        configId: record.id,
        actorId,
        action: 'CREATE',
        previousValue: null,
        newValue: finalValue
      });
    }

    // 4. Invalidate Cache
    await this._invalidateCache(scopeId);

    return {
      key: record.key,
      value: isSecret ? '********' : value, // Never return secret in response
      updatedAt: record.updatedAt
    };
  }

  /**
   * Get settings for a specific scope (Admin usage mainly)
   */
  async getScopeConfig(scope, scopeId) {
    const settings = await ConfigSetting.findAll({
      where: { scope, scopeId }
    });

    return this._processConfigList(settings, true); // true = mask secrets
  }

  // --- Helpers ---

  _validateValue(value, schema) {
    if (schema.type === 'number') {
      if (typeof value !== 'number') throw new Error('Value must be a number');
      if (schema.min !== undefined && value < schema.min) throw new Error(`Value must be >= ${schema.min}`);
      if (schema.max !== undefined && value > schema.max) throw new Error(`Value must be <= ${schema.max}`);
    }
    if (schema.type === 'string') {
      if (typeof value !== 'string') throw new Error('Value must be a string');
      if (schema.enum && !schema.enum.includes(value)) throw new Error(`Value must be one of: ${schema.enum.join(', ')}`);
      if (schema.pattern && !schema.pattern.test(value)) throw new Error('Value format is invalid');
      if (schema.minLength && value.length < schema.minLength) throw new Error(`Value must be at least ${schema.minLength} chars`);
    }
    if (schema.type === 'boolean') {
      if (typeof value !== 'boolean') throw new Error('Value must be a boolean');
    }
    if (schema.type === 'url') {
      // Basic URL check if no specific pattern
      if (!schema.pattern) {
        try {
          new URL(value);
        } catch {
          throw new Error('Value must be a valid URL');
        }
      } else if (!schema.pattern.test(value)) {
         throw new Error('Value must match URL pattern');
      }
    }
  }

  _processConfigList(list, maskSecrets = false) {
    const map = {};
    for (const item of list) {
      if (item.isSecret) {
        if (maskSecrets) {
          map[item.key] = '********';
        } else {
          try {
            map[item.key] = this._decrypt(item.value);
          } catch (e) {
            console.error(`Failed to decrypt key ${item.key}`, e);
            map[item.key] = null;
          }
        }
      } else {
        map[item.key] = item.value;
      }
    }
    return map;
  }

  _encrypt(text) {
    // Format: iv:authTag:encryptedContent
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY), iv);
    let encrypted = cipher.update(String(text), 'utf8', 'hex');
    encrypted += cipher.final('hex');
    const authTag = cipher.getAuthTag().toString('hex');
    return `${iv.toString('hex')}:${authTag}:${encrypted}`;
  }

  _decrypt(encryptedText) {
    // Format: iv:authTag:encryptedContent
    const parts = encryptedText.split(':');
    if (parts.length !== 3) throw new Error('Invalid encrypted format');

    const [ivHex, authTagHex, contentHex] = parts;
    const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY), Buffer.from(ivHex, 'hex'));
    decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));

    let decrypted = decipher.update(contentHex, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
  }

  async _invalidateCache(scopeId) {
    if (!redisClient || redisClient.status !== 'ready') return;

    // Pattern based invalidation is expensive in Redis (KEYS command).
    // Better strategy for this MVP:
    // Since we key by `resolved:projectId:userId`, we can't easily guess all keys to delete
    // when SYSTEM changes.
    // Ideally we use a version counter or specialized sets.
    // For MVP: We accept that updating SYSTEM config might take 5 mins to propagate
    // unless we iterate keys (bad for prod) or flushdb (bad for prod).
    // A workable middle ground for specific project updates:

    if (scopeId) {
        // If updating a project, we can try to guess user keys if we tracked them,
        // but let's just accept 5 min TTL for now as per requirements.
        // We can explicitly delete the specific scope cache if we were caching raw scopes separately.
    }

    // Note: In a real heavy-load system, we'd use a 'config_version' key that we append to cache keys
    // and increment on updates to instantly invalidate old namespaces.
  }
}

export default new ConfigService();
