// ClickHouse Telemetry Reporter
// Sends privacy-filtered telemetry to ClickHouse backend
// Optimized for batch inserts with low latency

export class ClickHouseReporter {
  constructor(config = {}) {
    this.config = config;
    this.enabled = config.clickhouseEnabled || false;
    this.endpoint = config.clickhouseEndpoint;
    this.database = config.clickhouseDatabase || 'phishprotect';
    this.username = config.clickhouseUsername;
    this.password = config.clickhousePassword;
    this.deviceId = null;
    this.organizationId = config.organizationId;

    // Batch configuration
    this.batchSize = config.clickhouseBatchSize || 1000;
    this.flushInterval = config.clickhouseFlushInterval || 30000; // 30 seconds
    this.maxRetries = config.clickhouseMaxRetries || 3;

    // Event buffers
    this.detectionBuffer = [];
    this.policyBuffer = [];
    this.performanceBuffer = [];
    this.auditBuffer = [];

    this.lastFlushTime = Date.now();
    this.totalEventsSent = 0;
    this.failedInserts = 0;
  }

  /**
   * Initialize ClickHouse reporter
   */
  async initialize() {
    if (!this.enabled) {
      console.log('ClickHouse reporter disabled');
      return false;
    }

    if (!this.endpoint) {
      console.error('ClickHouse endpoint not configured');
      return false;
    }

    // Get or create device ID
    this.deviceId = await this.getOrCreateDeviceId();

    // Test connection
    const connected = await this.testConnection();

    if (!connected) {
      console.warn('Failed to connect to ClickHouse - will retry on events');
    } else {
      // Register device
      await this.registerDevice();
    }

    // Start periodic flush
    this.startPeriodicFlush();

    console.log('ClickHouse reporter initialized successfully');
    return true;
  }

  /**
   * Get or create device ID
   */
  async getOrCreateDeviceId() {
    try {
      const result = await chrome.storage.local.get(['deviceId']);

      if (result.deviceId) {
        return result.deviceId;
      }

      // Generate new device ID
      const deviceId = `device_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 15)}`;
      await chrome.storage.local.set({ deviceId });

      return deviceId;
    } catch (error) {
      console.error('Failed to get/create device ID:', error);
      return `device_${Date.now().toString(36)}`;
    }
  }

  /**
   * Register device with ClickHouse
   */
  async registerDevice() {
    try {
      const manifest = chrome.runtime.getManifest();
      const browserInfo = this.getBrowserInfo();

      const deviceData = {
        device_id: this.deviceId,
        organization_id: this.organizationId,
        extension_version: manifest.version,
        browser_name: browserInfo.name,
        browser_version: browserInfo.version,
        enrollment_status: 'unknown',
        registered_at: this.toClickHouseDateTime(Date.now()),
        last_seen: this.toClickHouseDateTime(Date.now())
      };

      // Check for MDM attributes
      if (chrome.enterprise && chrome.enterprise.deviceAttributes) {
        try {
          const [serialNumber, assetId, location, hostname] = await Promise.all([
            chrome.enterprise.deviceAttributes.getDeviceSerialNumber().catch(() => null),
            chrome.enterprise.deviceAttributes.getDeviceAssetId().catch(() => null),
            chrome.enterprise.deviceAttributes.getDeviceAnnotatedLocation().catch(() => null),
            chrome.enterprise.deviceAttributes.getDeviceHostname().catch(() => null)
          ]);

          if (serialNumber || assetId || location || hostname) {
            deviceData.serial_number = serialNumber;
            deviceData.asset_id = assetId;
            deviceData.annotated_location = location;
            deviceData.hostname = hostname;
            deviceData.enrollment_status = 'managed';
          }
        } catch (error) {
          console.warn('Failed to fetch MDM attributes:', error);
        }
      }

      await this.insertData('devices', [deviceData]);

      console.log('Device registered with ClickHouse');
      return true;
    } catch (error) {
      console.error('Failed to register device:', error);
      return false;
    }
  }

  /**
   * Report detection event
   * @param {object} detection - Detection result
   * @param {object} context - Additional context
   */
  reportDetection(detection, context = {}) {
    if (!this.enabled) return;

    // Aggregate to hour for privacy
    const eventHour = this.aggregateToHour(Date.now());

    const event = {
      event_hour: this.toClickHouseDateTime(eventHour),
      device_id: this.deviceId,
      organization_id: this.organizationId,
      risk_score: detection.riskScore || 0,
      risk_level: this.getRiskLevel(detection.riskScore),
      finding_count: detection.findings?.length || 0,
      matched: detection.matched ? 1 : 0,
      detectors: detection.findings?.map(f => f.detector).filter(Boolean) || [],
      categories: [...new Set(detection.findings?.map(f => f.category).filter(Boolean))] || [],
      techniques: [...new Set(detection.findings?.map(f => f.metadata?.technique).filter(Boolean))] || [],
      severity_critical: this.countSeverity(detection.findings, 'critical'),
      severity_high: this.countSeverity(detection.findings, 'high'),
      severity_medium: this.countSeverity(detection.findings, 'medium'),
      severity_low: this.countSeverity(detection.findings, 'low'),
      session_id: context.sessionId ? this.hashValue(context.sessionId) : ''
    };

    this.detectionBuffer.push(event);

    // Flush if buffer is full or critical severity
    if (this.detectionBuffer.length >= this.batchSize || detection.riskScore >= 15) {
      this.flushDetections();
    }
  }

  /**
   * Report policy decision
   * @param {object} decision - Policy decision
   * @param {object} context - Additional context
   */
  reportPolicyDecision(decision, context = {}) {
    if (!this.enabled) return;

    const eventHour = this.aggregateToHour(Date.now());

    const event = {
      event_hour: this.toClickHouseDateTime(eventHour),
      device_id: this.deviceId,
      organization_id: this.organizationId,
      policy_id: context.policyId || 'default',
      action: decision.action,
      risk_score: decision.riskScore || 0,
      risk_level: this.getRiskLevel(decision.riskScore),
      enforcement_type: decision.enforcement?.type || null,
      matched_rule_count: decision.matchedRules?.length || 0,
      user_override: decision.userOverride ? 1 : 0,
      session_id: context.sessionId ? this.hashValue(context.sessionId) : ''
    };

    this.policyBuffer.push(event);

    if (this.policyBuffer.length >= this.batchSize) {
      this.flushPolicyDecisions();
    }
  }

  /**
   * Report detector performance
   * @param {string} detectorName - Detector name
   * @param {number} latency - Execution time in ms
   * @param {boolean} matched - Whether detector matched
   */
  reportPerformance(detectorName, latency, matched = false) {
    if (!this.enabled) return;

    const eventHour = this.aggregateToHour(Date.now());

    const event = {
      event_hour: this.toClickHouseDateTime(eventHour),
      device_id: this.deviceId,
      organization_id: this.organizationId,
      detector_name: detectorName,
      latency_ms: Math.round(latency),
      matched: matched ? 1 : 0,
      execution_count: 1,
      hit_count: matched ? 1 : 0
    };

    this.performanceBuffer.push(event);

    if (this.performanceBuffer.length >= this.batchSize) {
      this.flushPerformance();
    }
  }

  /**
   * Report audit log
   * @param {object} logEntry - Audit log entry
   */
  reportAuditLog(logEntry) {
    if (!this.enabled) return;

    const event = {
      event_time: this.toClickHouseDateTime(logEntry.timestamp || Date.now()),
      device_id: this.deviceId,
      organization_id: this.organizationId,
      log_id: logEntry.id,
      log_type: logEntry.type,
      action: logEntry.action || null,
      risk_score: logEntry.riskScore || 0,
      detector_count: logEntry.detectorCount || 0,
      policy_id: logEntry.policyId || null,
      matched_rules: logEntry.matchedRules || [],
      categories: logEntry.categories || [],
      techniques: logEntry.techniques || [],
      session_id: logEntry.session || null
    };

    this.auditBuffer.push(event);

    if (this.auditBuffer.length >= this.batchSize) {
      this.flushAuditLogs();
    }
  }

  /**
   * Flush detection events to ClickHouse
   */
  async flushDetections() {
    if (this.detectionBuffer.length === 0) return;

    const batch = [...this.detectionBuffer];
    this.detectionBuffer = [];

    try {
      await this.insertData('detection_events', batch);
      this.totalEventsSent += batch.length;
      console.log(`Flushed ${batch.length} detection events to ClickHouse`);
    } catch (error) {
      console.error('Failed to flush detection events:', error);
      this.failedInserts += batch.length;

      // Re-add to buffer with limit
      if (this.detectionBuffer.length < this.batchSize * 2) {
        this.detectionBuffer.unshift(...batch);
      }
    }
  }

  /**
   * Flush policy decision events to ClickHouse
   */
  async flushPolicyDecisions() {
    if (this.policyBuffer.length === 0) return;

    const batch = [...this.policyBuffer];
    this.policyBuffer = [];

    try {
      await this.insertData('policy_decisions', batch);
      this.totalEventsSent += batch.length;
      console.log(`Flushed ${batch.length} policy decisions to ClickHouse`);
    } catch (error) {
      console.error('Failed to flush policy decisions:', error);
      this.failedInserts += batch.length;

      if (this.policyBuffer.length < this.batchSize * 2) {
        this.policyBuffer.unshift(...batch);
      }
    }
  }

  /**
   * Flush performance events to ClickHouse
   */
  async flushPerformance() {
    if (this.performanceBuffer.length === 0) return;

    const batch = [...this.performanceBuffer];
    this.performanceBuffer = [];

    try {
      await this.insertData('detector_performance', batch);
      this.totalEventsSent += batch.length;
      console.log(`Flushed ${batch.length} performance events to ClickHouse`);
    } catch (error) {
      console.error('Failed to flush performance events:', error);
      this.failedInserts += batch.length;

      if (this.performanceBuffer.length < this.batchSize * 2) {
        this.performanceBuffer.unshift(...batch);
      }
    }
  }

  /**
   * Flush audit logs to ClickHouse
   */
  async flushAuditLogs() {
    if (this.auditBuffer.length === 0) return;

    const batch = [...this.auditBuffer];
    this.auditBuffer = [];

    try {
      await this.insertData('audit_logs', batch);
      this.totalEventsSent += batch.length;
      console.log(`Flushed ${batch.length} audit logs to ClickHouse`);
    } catch (error) {
      console.error('Failed to flush audit logs:', error);
      this.failedInserts += batch.length;

      if (this.auditBuffer.length < this.batchSize * 2) {
        this.auditBuffer.unshift(...batch);
      }
    }
  }

  /**
   * Insert data into ClickHouse table
   * @param {string} table - Table name
   * @param {array} data - Data to insert
   */
  async insertData(table, data) {
    if (data.length === 0) return;

    // Build INSERT query
    const columns = Object.keys(data[0]);
    const query = `INSERT INTO ${this.database}.${table} (${columns.join(', ')}) FORMAT JSONEachRow`;

    // Convert data to NDJSON (newline-delimited JSON)
    const ndjson = data.map(row => JSON.stringify(row)).join('\n');

    // Send to ClickHouse
    await this.executeQuery(query, ndjson);
  }

  /**
   * Execute ClickHouse query
   * @param {string} query - SQL query
   * @param {string} data - Data for INSERT queries
   */
  async executeQuery(query, data = null) {
    const url = `${this.endpoint}/?database=${this.database}`;

    const headers = {
      'Content-Type': 'application/x-ndjson'
    };

    // Add authentication
    if (this.username && this.password) {
      const auth = btoa(`${this.username}:${this.password}`);
      headers['Authorization'] = `Basic ${auth}`;
    }

    const options = {
      method: 'POST',
      headers,
      body: data ? `${query}\n${data}` : query
    };

    let retries = 0;
    while (retries <= this.maxRetries) {
      try {
        const response = await fetch(url, options);

        if (!response.ok) {
          const errorText = await response.text();
          throw new Error(`ClickHouse error: ${response.status} - ${errorText}`);
        }

        return await response.text();
      } catch (error) {
        retries++;
        if (retries > this.maxRetries) {
          throw error;
        }

        // Exponential backoff
        const delay = Math.min(1000 * Math.pow(2, retries), 10000);
        console.warn(`ClickHouse request failed, retrying in ${delay}ms (attempt ${retries}/${this.maxRetries})`);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  /**
   * Test connection to ClickHouse
   */
  async testConnection() {
    try {
      await this.executeQuery('SELECT 1');
      console.log('ClickHouse connection test successful');
      return true;
    } catch (error) {
      console.error('ClickHouse connection test failed:', error);
      return false;
    }
  }

  /**
   * Get risk level from risk score
   * @param {number} riskScore - Risk score
   * @returns {string} - Risk level
   */
  getRiskLevel(riskScore) {
    if (riskScore >= 15) return 'critical';
    if (riskScore >= 10) return 'high';
    if (riskScore >= 5) return 'medium';
    return 'low';
  }

  /**
   * Count findings by severity
   * @param {array} findings - Findings
   * @param {string} severity - Severity level
   * @returns {number} - Count
   */
  countSeverity(findings, severity) {
    if (!findings) return 0;
    return findings.filter(f => f.severity === severity).length;
  }

  /**
   * Aggregate timestamp to hour
   * @param {number} timestamp - Timestamp in ms
   * @returns {number} - Timestamp rounded to hour
   */
  aggregateToHour(timestamp) {
    return Math.floor(timestamp / 3600000) * 3600000;
  }

  /**
   * Convert timestamp to ClickHouse DateTime format
   * @param {number} timestamp - Timestamp in ms
   * @returns {string} - ClickHouse DateTime
   */
  toClickHouseDateTime(timestamp) {
    const date = new Date(timestamp);
    return date.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '');
  }

  /**
   * Hash value for privacy
   * @param {string} value - Value to hash
   * @returns {string} - Hashed value
   */
  hashValue(value) {
    if (!value) return '';

    let hash = 0;
    for (let i = 0; i < value.length; i++) {
      const char = value.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }

    return `hash_${Math.abs(hash).toString(36)}`;
  }

  /**
   * Get browser information
   * @returns {object} - Browser info
   */
  getBrowserInfo() {
    const userAgent = navigator.userAgent;
    let name = 'unknown';
    let version = 'unknown';

    if (userAgent.includes('Chrome')) {
      name = 'chrome';
      const match = userAgent.match(/Chrome\/(\d+)/);
      version = match ? match[1] : 'unknown';
    } else if (userAgent.includes('Firefox')) {
      name = 'firefox';
      const match = userAgent.match(/Firefox\/(\d+)/);
      version = match ? match[1] : 'unknown';
    } else if (userAgent.includes('Edge')) {
      name = 'edge';
      const match = userAgent.match(/Edge\/(\d+)/);
      version = match ? match[1] : 'unknown';
    }

    return { name, version };
  }

  /**
   * Start periodic flush
   */
  startPeriodicFlush() {
    if (this.flushIntervalId) {
      clearInterval(this.flushIntervalId);
    }

    this.flushIntervalId = setInterval(async () => {
      await this.flushAll();
    }, this.flushInterval);
  }

  /**
   * Stop periodic flush
   */
  stopPeriodicFlush() {
    if (this.flushIntervalId) {
      clearInterval(this.flushIntervalId);
      this.flushIntervalId = null;
    }
  }

  /**
   * Flush all buffers
   */
  async flushAll() {
    await Promise.all([
      this.flushDetections(),
      this.flushPolicyDecisions(),
      this.flushPerformance(),
      this.flushAuditLogs()
    ]);

    this.lastFlushTime = Date.now();
  }

  /**
   * Force immediate flush
   */
  async forceFlush() {
    console.log('Force flushing all buffers to ClickHouse...');
    await this.flushAll();
  }

  /**
   * Get statistics
   * @returns {object} - Reporter statistics
   */
  getStatistics() {
    return {
      enabled: this.enabled,
      endpoint: this.endpoint,
      database: this.database,
      deviceId: this.deviceId,
      organizationId: this.organizationId,
      buffered: {
        detections: this.detectionBuffer.length,
        policies: this.policyBuffer.length,
        performance: this.performanceBuffer.length,
        audit: this.auditBuffer.length
      },
      totalEventsSent: this.totalEventsSent,
      failedInserts: this.failedInserts,
      lastFlushTime: this.lastFlushTime,
      batchSize: this.batchSize,
      flushInterval: this.flushInterval
    };
  }

  /**
   * Clean up and stop reporter
   */
  async cleanup() {
    this.stopPeriodicFlush();

    // Flush any remaining events
    await this.flushAll();

    console.log('ClickHouse reporter cleaned up');
  }
}
