/**
 * Suppression Manager
 * Central class for managing all suppression logic
 */

import { minimatch } from 'minimatch'
import type { Vulnerability, ScanFile } from '../../shared/types'
import type {
  SuppressionConfig,
  SuppressionMatch,
  SuppressionResult,
  SuppressedVulnerability,
  InlineSuppression,
  RuleSuppression,
  FindingSuppression,
} from './types'
import { loadSuppressionConfig, isExpired } from './config-loader'
import { parseInlineSuppressions, isLineSuppressed } from './inline-parser'
import { computeFindingHash, normalizePathForHash } from './hash'

/**
 * Options for SuppressionManager
 */
export interface SuppressionManagerOptions {
  /** Path to the project root (for finding config files) */
  projectPath: string
  /** Optional pre-loaded config (skips file loading) */
  config?: SuppressionConfig
  /** Whether to include expired suppressions (for auditing) */
  includeExpired?: boolean
}

/**
 * SuppressionManager handles all suppression logic
 *
 * Priority order for suppressions:
 * 1. Inline comment suppressions (highest priority)
 * 2. Config file finding suppressions (by hash)
 * 3. Config file rule suppressions (by category)
 */
export class SuppressionManager {
  private config: SuppressionConfig
  private configPath: string | undefined
  private configErrors: string[]
  private includeExpired: boolean
  private projectPath: string

  // Cache for inline suppressions by file path
  private inlineSuppressionCache: Map<string, Map<number, InlineSuppression>> = new Map()

  constructor(options: SuppressionManagerOptions) {
    this.projectPath = options.projectPath
    this.includeExpired = options.includeExpired ?? false

    if (options.config) {
      this.config = options.config
      this.configPath = undefined
      this.configErrors = []
    } else {
      const result = loadSuppressionConfig(options.projectPath)
      this.config = result.config
      this.configPath = result.configPath
      this.configErrors = result.errors
    }
  }

  /**
   * Get configuration errors (if any)
   */
  getConfigErrors(): string[] {
    return this.configErrors
  }

  /**
   * Get the path to the config file (if found)
   */
  getConfigPath(): string | undefined {
    return this.configPath
  }

  /**
   * Check if a file path should be ignored entirely
   */
  isPathIgnored(filePath: string): boolean {
    if (!this.config.ignore || this.config.ignore.length === 0) {
      return false
    }

    const normalizedPath = normalizePathForHash(filePath)

    return this.config.ignore.some(pattern =>
      minimatch(normalizedPath, pattern, { dot: true })
    )
  }

  /**
   * Parse inline suppressions for a file
   */
  private getInlineSuppressions(
    filePath: string,
    content: string
  ): Map<number, InlineSuppression> {
    // Check cache first
    const cached = this.inlineSuppressionCache.get(filePath)
    if (cached) {
      return cached
    }

    // Parse and cache
    const suppressions = parseInlineSuppressions(content)
    this.inlineSuppressionCache.set(filePath, suppressions)
    return suppressions
  }

  /**
   * Check if a finding is suppressed
   *
   * Priority:
   * 1. Inline comment (highest)
   * 2. Config finding suppression (by hash)
   * 3. Config rule suppression (by category)
   */
  isFindingSuppressed(
    finding: Vulnerability,
    fileContent?: string
  ): SuppressionMatch {
    const hash = computeFindingHash(finding)
    const normalizedPath = normalizePathForHash(finding.filePath)

    // 1. Check inline suppressions (if file content provided)
    if (fileContent) {
      const inlineSuppressions = this.getInlineSuppressions(finding.filePath, fileContent)
      const inlineSuppression = isLineSuppressed(
        inlineSuppressions,
        finding.lineNumber,
        finding.category
      )

      if (inlineSuppression) {
        return {
          suppressed: true,
          match: {
            type: 'inline',
            reason: inlineSuppression.reason,
          },
          hash,
        }
      }
    }

    // 2. Check config finding suppressions (by hash)
    const findingSuppressions = this.config.suppressions?.findings || []
    const findingSuppression = findingSuppressions.find(s => s.hash === hash)

    if (findingSuppression) {
      const expired = isExpired(findingSuppression.expires)

      if (expired && !this.includeExpired) {
        // Expired - not suppressed
        return {
          suppressed: false,
          match: {
            type: 'config-finding',
            reason: findingSuppression.reason,
            expires: findingSuppression.expires,
            expired: true,
          },
          hash,
        }
      }

      return {
        suppressed: true,
        match: {
          type: 'config-finding',
          reason: findingSuppression.reason,
          expires: findingSuppression.expires,
          expired,
        },
        hash,
      }
    }

    // 3. Check config rule suppressions (by category)
    const ruleSuppressions = this.config.suppressions?.rules || []
    const ruleSuppression = ruleSuppressions.find(s => {
      // Must match category
      if (s.category !== finding.category) {
        return false
      }

      // If paths specified, must match one of them
      if (s.paths && s.paths.length > 0) {
        const matchesPath = s.paths.some(pattern =>
          minimatch(normalizedPath, pattern, { dot: true })
        )
        if (!matchesPath) {
          return false
        }
      }

      return true
    })

    if (ruleSuppression) {
      const expired = isExpired(ruleSuppression.expires)

      if (expired && !this.includeExpired) {
        // Expired - not suppressed
        return {
          suppressed: false,
          match: {
            type: 'config-rule',
            reason: ruleSuppression.reason,
            expires: ruleSuppression.expires,
            expired: true,
          },
          hash,
        }
      }

      return {
        suppressed: true,
        match: {
          type: 'config-rule',
          reason: ruleSuppression.reason,
          expires: ruleSuppression.expires,
          expired,
        },
        hash,
      }
    }

    // Not suppressed
    return {
      suppressed: false,
      hash,
    }
  }

  /**
   * Apply suppressions to a list of findings
   */
  applySuppressions(
    findings: Vulnerability[],
    files: ScanFile[]
  ): SuppressionResult {
    // Build file content lookup
    const fileContentMap = new Map<string, string>()
    for (const file of files) {
      fileContentMap.set(normalizePathForHash(file.path), file.content)
    }

    const passed: Vulnerability[] = []
    const suppressed: SuppressedVulnerability[] = []
    let expiredCount = 0

    const stats = {
      total: findings.length,
      inlineSuppressed: 0,
      configFindingSuppressed: 0,
      configRuleSuppressed: 0,
      expired: 0,
    }

    for (const finding of findings) {
      // Get file content for inline suppression checking
      const normalizedPath = normalizePathForHash(finding.filePath)
      const fileContent = fileContentMap.get(normalizedPath)

      const result = this.isFindingSuppressed(finding, fileContent)

      if (result.suppressed) {
        suppressed.push({
          vulnerability: {
            id: finding.id,
            filePath: finding.filePath,
            lineNumber: finding.lineNumber,
            category: finding.category,
            severity: finding.severity,
            title: finding.title,
          },
          suppression: {
            type: result.match!.type,
            reason: result.match!.reason,
            expires: result.match!.expires,
            hash: result.hash,
          },
        })

        // Update stats
        switch (result.match!.type) {
          case 'inline':
            stats.inlineSuppressed++
            break
          case 'config-finding':
            stats.configFindingSuppressed++
            break
          case 'config-rule':
            stats.configRuleSuppressed++
            break
        }
      } else {
        passed.push(finding)

        // Track expired suppressions
        if (result.match?.expired) {
          expiredCount++
          stats.expired++
        }
      }
    }

    return {
      findings: passed,
      suppressed,
      expiredSuppressions: expiredCount,
      stats,
    }
  }

  /**
   * Get all suppressions from config
   */
  getAllSuppressions(): {
    rules: RuleSuppression[]
    findings: FindingSuppression[]
  } {
    return {
      rules: this.config.suppressions?.rules || [],
      findings: this.config.suppressions?.findings || [],
    }
  }

  /**
   * Get ignore patterns from config
   */
  getIgnorePatterns(): string[] {
    return this.config.ignore || []
  }

  /**
   * Check if suppression system is active (has any suppressions)
   */
  hasSuppressions(): boolean {
    const rules = this.config.suppressions?.rules || []
    const findings = this.config.suppressions?.findings || []
    const ignore = this.config.ignore || []

    return rules.length > 0 || findings.length > 0 || ignore.length > 0
  }

  /**
   * Get summary of suppression configuration
   */
  getSummary(): {
    configPath: string | undefined
    ruleCount: number
    findingCount: number
    ignorePatternCount: number
    hasErrors: boolean
  } {
    return {
      configPath: this.configPath,
      ruleCount: this.config.suppressions?.rules?.length || 0,
      findingCount: this.config.suppressions?.findings?.length || 0,
      ignorePatternCount: this.config.ignore?.length || 0,
      hasErrors: this.configErrors.length > 0,
    }
  }

  /**
   * Clear the inline suppression cache
   * (useful if files have been modified)
   */
  clearCache(): void {
    this.inlineSuppressionCache.clear()
  }
}
