// PhishProtect Detection Reporter
// Fires on blocking detections, sends detailed event data to the cloud

export class DetectionReporter {
  constructor(config = {}) {
    this.apiUrl = config.apiUrl || 'https://api.nithic.ai';
    this.enabled = config.enabled !== false;
    this.shareUrls = config.shareUrls !== false;
    this.screenshotMode = config.screenshotMode || 'blurred'; // 'full', 'blurred', 'off'
    this.deviceId = config.deviceId || null;
    this.extensionVersion = config.extensionVersion || '2.0.0';
    this.systemContext = config.systemContext || null;
    this.frameChainBuilder = config.frameChainBuilder || null;

    // Batch queue
    this.queue = [];
    this.batchSize = 20;
    this.flushIntervalMs = 30000; // 30 seconds
    this.flushTimer = null;

    // Start flush timer
    this._startFlushTimer();
  }

  /**
   * Report a detection event
   * @param {Object} detection - Detection result
   * @param {Object} decision - Policy decision
   * @param {number} tabId - Chrome tab ID
   * @param {string} url - Page URL
   * @param {number} [frameId=0] - Chrome frame ID (0 = main frame)
   */
  async reportBlockingDetection(detection, decision, tabId, url, frameId = 0) {
    if (!this.enabled) return;

    const event = {
      device_id: this.deviceId,
      extension_version: this.extensionVersion,
      url: this.shareUrls ? url : null,
      url_hash: url ? await this._hashUrl(url) : null,
      domain: this.shareUrls ? this._extractDomain(url) : null,
      page_title: null,
      risk_score: detection.riskScore || 0,
      risk_level: detection.riskLevel || 'low',
      user_agent: this.systemContext?.user_agent || null,

      // Each finding carries its own evidence and rule_version
      findings: this._buildFindings(detection),

      // Decision context
      action: decision.action,
      policy_id: decision.policyId || null,
      reason: decision.reason || null,
      cloud_escalated: decision.cloudEscalated || false,

      // System context
      system: this.systemContext || null,

      // Analysis metadata
      analysis: detection.metadata ? {
        detectors_run: detection.metadata.detectorsRun,
        detectors_matched: detection.metadata.detectorsMatched,
        latency_ms: detection.metadata.latency,
        target_type: detection.metadata.targetType
      } : null,

      pattern_id: detection.patternId || null,
      technique_id: detection.techniqueId || null,
      rule_id: detection.ruleId || this._extractRuleId(detection.findings) || null,

      // v3: Feature store snapshot (signal-based features)
      features: detection.features || null,

      // Frame chain evidence (from FrameChainBuilder)
      frame_chain: null
    };

    // Get tab info (used for page title and fallback frame chain)
    let tabInfo = null;
    if (tabId) {
      try {
        tabInfo = await chrome.tabs.get(tabId);
        event.page_title = tabInfo.title || null;
      } catch {}
    }

    // Build frame chain if available
    if (this.frameChainBuilder && tabId) {
      try {
        const chain = this.frameChainBuilder.getChain(tabId, frameId);
        event.frame_chain = chain;
        console.log(`DetectionReporter: frame chain for tab ${tabId} frame ${frameId}: ${chain.length} frames`);
      } catch (err) {
        console.warn('DetectionReporter: Failed to get frame chain:', err.message);
      }
    } else {
      console.warn(`DetectionReporter: No frame chain builder (${!!this.frameChainBuilder}) or tabId (${tabId})`);
    }

    // Fallback: if frame chain is empty (e.g. after SW restart with no session data),
    // populate at least the top frame from tab info so we always have context
    if ((!event.frame_chain || event.frame_chain.length === 0) && tabInfo && tabInfo.url) {
      let origin = null;
      try { origin = new URL(tabInfo.url).origin; } catch {}
      event.frame_chain = [{
        frameId: 0,
        url: this.shareUrls ? tabInfo.url : null,
        origin: this.shareUrls ? origin : null,
        isTopFrame: true,
        documentTitle: tabInfo.title || null,
        dimensions: null,
        displayRatio: null,
        computedStyles: null,
        viewport: null,
        technologies: null,
        responseHeaders: null,
        setCookieNames: null
      }];
      console.log('DetectionReporter: Used tab info fallback for frame chain');
    }

    // Capture screenshot based on mode
    if (this.screenshotMode !== 'off' && tabId) {
      try {
        const dataUrl = await chrome.tabs.captureVisibleTab(null, { format: 'jpeg', quality: 70 });

        if (this.screenshotMode === 'blurred') {
          event.screenshot = await this._blurScreenshot(dataUrl);
        } else {
          event.screenshot = dataUrl.replace(/^data:image\/\w+;base64,/, '');
        }
        event.screenshot_mode = this.screenshotMode;
      } catch (err) {
        console.warn('DetectionReporter: Failed to capture screenshot:', err.message);
      }
    }

    this.queue.push(event);

    // Flush immediately - detections are rare, no need to wait
    await this.flush();
  }

  /**
   * Flush queued events to the server
   */
  async flush() {
    if (this.queue.length === 0) return;

    const events = this.queue.splice(0, this.batchSize);

    try {
      if (events.length === 1) {
        const response = await fetch(`${this.apiUrl}/v1/telemetry/detection`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(events[0])
        });

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        const result = await response.json();
        console.log('DetectionReporter: Event sent:', result.event_id);
      } else {
        const response = await fetch(`${this.apiUrl}/v1/telemetry/batch`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ events })
        });

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        const result = await response.json();
        console.log(`DetectionReporter: Batch sent: ${result.inserted} events`);
      }
    } catch (error) {
      console.error('DetectionReporter: Failed to send events:', error.message);
      this.queue.unshift(...events);
      if (this.queue.length > 200) {
        this.queue.length = 200;
      }
    }
  }

  /**
   * Update configuration
   */
  updateConfig(config) {
    if (config.apiUrl !== undefined) this.apiUrl = config.apiUrl;
    if (config.enabled !== undefined) this.enabled = config.enabled;
    if (config.shareUrls !== undefined) this.shareUrls = config.shareUrls;
    if (config.screenshotMode !== undefined) this.screenshotMode = config.screenshotMode;
    if (config.deviceId !== undefined) this.deviceId = config.deviceId;
    if (config.frameChainBuilder !== undefined) this.frameChainBuilder = config.frameChainBuilder;
  }

  /**
   * Get reporter status
   */
  getStatus() {
    return {
      enabled: this.enabled,
      queueLength: this.queue.length,
      shareUrls: this.shareUrls,
      screenshotMode: this.screenshotMode
    };
  }

  // ---- Private helpers ----

  /**
   * Build findings array - each finding carries its own evidence and rule_version
   */
  _buildFindings(detection) {
    const clipboardEvidence = detection.clipboardEvidence || null;

    return (detection.findings || []).map(f => {
      const finding = {
        detector: f.detector,
        type: f.type,
        severity: f.severity,
        weight: f.weight,
        category: f.category,
        rule_id: f.ruleId || f.rule_id || null,
        rule_version: f.metadata?.version || f.metadata?.ruleVersion || null,
        pattern: f.pattern || null,
        matched: f.matched || null,
        description: f.description || null,
        evidence: this._buildFindingEvidence(f, clipboardEvidence)
      };

      return finding;
    });
  }

  /**
   * Build per-finding evidence: the signals/data that triggered this specific finding
   */
  _buildFindingEvidence(finding, clipboardEvidence) {
    const evidence = {};

    // Enhanced rule signal results - what each signal matched
    if (finding.metadata?.signalResults) {
      evidence.signal_results = finding.metadata.signalResults;
    }

    // Extracted variables (parsed domains, brand matches, etc.)
    if (finding.metadata?.variables) {
      evidence.variables = finding.metadata.variables;
    }

    // Rule metadata
    if (finding.metadata?.confidence !== undefined) {
      evidence.confidence = finding.metadata.confidence;
    }
    if (finding.metadata?.ruleName) {
      evidence.rule_name = finding.metadata.ruleName;
    }
    if (finding.metadata?.rulesetId) {
      evidence.ruleset_id = finding.metadata.rulesetId;
    }
    if (finding.metadata?.action) {
      evidence.action = finding.metadata.action;
    }
    if (finding.metadata?.technique) {
      evidence.technique = finding.metadata.technique;
    }

    // Clipboard evidence - attach to clipboard-injection findings
    if (finding.detector === 'clipboard-injection' && clipboardEvidence) {
      evidence.clipboard_source = clipboardEvidence.source;
      evidence.clipboard_content = clipboardEvidence.contentPreview;
      evidence.threat_types = clipboardEvidence.threatTypes;
      if (clipboardEvidence.stackTrace) {
        evidence.stack_trace = clipboardEvidence.stackTrace;
      }
      if (clipboardEvidence.sourceSnippets && clipboardEvidence.sourceSnippets.length > 0) {
        evidence.source_snippets = clipboardEvidence.sourceSnippets;
      }
      if (clipboardEvidence.inlineScripts && clipboardEvidence.inlineScripts.length > 0) {
        evidence.inline_scripts = clipboardEvidence.inlineScripts;
      }
      if (clipboardEvidence.dynamicScripts && clipboardEvidence.dynamicScripts.length > 0) {
        evidence.dynamic_scripts = clipboardEvidence.dynamicScripts;
      }
    }

    // Stack trace from detection trigger (DOM mutation, etc.)
    if (finding.stackTrace || finding.stack_trace) {
      evidence.stack_trace = finding.stackTrace || finding.stack_trace;
    }

    return Object.keys(evidence).length > 0 ? evidence : null;
  }

  /**
   * Extract the best rule identifier from findings.
   * Priority: rulesetId from metadata > ruleId on finding > detector name
   */
  _extractRuleId(findings) {
    if (!findings || findings.length === 0) return null;

    for (const f of findings) {
      if (f.metadata?.rulesetId) return f.metadata.rulesetId;
    }
    for (const f of findings) {
      if (f.ruleId || f.rule_id) return f.ruleId || f.rule_id;
    }

    return findings[0]?.detector || null;
  }

  /**
   * Blur a screenshot by downscaling to a tiny size then re-encoding
   */
  async _blurScreenshot(dataUrl) {
    try {
      const response = await fetch(dataUrl);
      const blob = await response.blob();
      const bitmap = await createImageBitmap(blob);

      const scale = 160 / bitmap.width;
      const w = Math.round(bitmap.width * scale);
      const h = Math.round(bitmap.height * scale);

      const canvas = new OffscreenCanvas(w, h);
      const ctx = canvas.getContext('2d');
      ctx.drawImage(bitmap, 0, 0, w, h);
      bitmap.close();

      const blurredBlob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.4 });
      const buffer = await blurredBlob.arrayBuffer();
      const bytes = new Uint8Array(buffer);
      let binary = '';
      for (let i = 0; i < bytes.length; i++) {
        binary += String.fromCharCode(bytes[i]);
      }
      return btoa(binary);
    } catch (err) {
      console.warn('DetectionReporter: Failed to blur screenshot:', err.message);
      return dataUrl.replace(/^data:image\/\w+;base64,/, '');
    }
  }

  _startFlushTimer() {
    if (this.flushTimer) clearInterval(this.flushTimer);
    this.flushTimer = setInterval(() => this.flush(), this.flushIntervalMs);
  }

  async _hashUrl(url) {
    const encoder = new TextEncoder();
    const data = encoder.encode(url);
    const hashBuffer = await crypto.subtle.digest('SHA-256', data);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  }

  _extractDomain(url) {
    try {
      return new URL(url).hostname;
    } catch {
      return null;
    }
  }

  destroy() {
    if (this.flushTimer) {
      clearInterval(this.flushTimer);
      this.flushTimer = null;
    }
  }
}
