package storage

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"time"
)

// Finding represents a detected API key with metadata.
// KeyValue is always plaintext in this struct — encryption happens at the storage boundary.
type Finding struct {
	ID           int64
	ScanID       int64
	ProviderName string
	KeyValue     string // plaintext — encrypted before storage, decrypted after retrieval
	KeyMasked    string // first8...last4, stored plaintext
	Confidence   string
	SourcePath   string
	SourceType   string
	LineNumber   int
	CreatedAt    time.Time

	// Verification fields populated by the Phase 5 verifier. Zero values mean
	// the finding has not been verified.
	Verified       bool
	VerifyStatus   string // "live", "dead", "rate_limited", "error", "unknown"
	VerifyHTTPCode int
	VerifyMetadata map[string]string
}

// MaskKey returns the masked form of a key: first 8 chars + "..." + last 4 chars.
// If the key is too short (< 12 chars), returns the full key masked with asterisks.
func MaskKey(key string) string {
	if len(key) < 12 {
		return "****"
	}
	return key[:8] + "..." + key[len(key)-4:]
}

// SaveFinding encrypts the finding's KeyValue and persists the finding to the database.
// encKey must be a 32-byte AES-256 key (from DeriveKey).
func (db *DB) SaveFinding(f Finding, encKey []byte) (int64, error) {
	encrypted, err := Encrypt([]byte(f.KeyValue), encKey)
	if err != nil {
		return 0, fmt.Errorf("encrypting key value: %w", err)
	}

	masked := f.KeyMasked
	if masked == "" {
		masked = MaskKey(f.KeyValue)
	}

	// Use NULL for scan_id when not set (zero value) to satisfy FK constraint
	var scanID interface{}
	if f.ScanID != 0 {
		scanID = f.ScanID
	} else {
		scanID = sql.NullInt64{}
	}

	// Serialize verify metadata as JSON (NULL when nil) to match schema.
	var metaJSON interface{}
	if f.VerifyMetadata != nil {
		b, err := json.Marshal(f.VerifyMetadata)
		if err != nil {
			return 0, fmt.Errorf("marshaling verify metadata: %w", err)
		}
		metaJSON = string(b)
	} else {
		metaJSON = sql.NullString{}
	}

	verifiedInt := 0
	if f.Verified {
		verifiedInt = 1
	}

	res, err := db.sql.Exec(
		`INSERT INTO findings (
			scan_id, provider_name, key_value, key_masked, confidence,
			source_path, source_type, line_number,
			verified, verify_status, verify_http_code, verify_metadata_json
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		scanID, f.ProviderName, encrypted, masked, f.Confidence,
		f.SourcePath, f.SourceType, f.LineNumber,
		verifiedInt, f.VerifyStatus, f.VerifyHTTPCode, metaJSON,
	)
	if err != nil {
		return 0, fmt.Errorf("inserting finding: %w", err)
	}
	return res.LastInsertId()
}

// ListFindings retrieves all findings, decrypting key values using encKey.
// encKey must be the same 32-byte key used during SaveFinding.
func (db *DB) ListFindings(encKey []byte) ([]Finding, error) {
	rows, err := db.sql.Query(
		`SELECT id, scan_id, provider_name, key_value, key_masked, confidence,
                source_path, source_type, line_number,
                verified, verify_status, verify_http_code, verify_metadata_json,
                created_at
         FROM findings ORDER BY created_at DESC`,
	)
	if err != nil {
		return nil, fmt.Errorf("querying findings: %w", err)
	}
	defer rows.Close()

	var findings []Finding
	for rows.Next() {
		var f Finding
		var encrypted []byte
		var createdAt string
		var scanID sql.NullInt64
		var verifiedInt int
		var metaJSON sql.NullString
		if err := rows.Scan(
			&f.ID, &scanID, &f.ProviderName, &encrypted, &f.KeyMasked,
			&f.Confidence, &f.SourcePath, &f.SourceType, &f.LineNumber,
			&verifiedInt, &f.VerifyStatus, &f.VerifyHTTPCode, &metaJSON,
			&createdAt,
		); err != nil {
			return nil, fmt.Errorf("scanning finding row: %w", err)
		}
		if scanID.Valid {
			f.ScanID = scanID.Int64
		}
		f.Verified = verifiedInt != 0
		if metaJSON.Valid && metaJSON.String != "" {
			m := map[string]string{}
			if err := json.Unmarshal([]byte(metaJSON.String), &m); err != nil {
				return nil, fmt.Errorf("unmarshaling verify metadata for finding %d: %w", f.ID, err)
			}
			f.VerifyMetadata = m
		}
		plain, err := Decrypt(encrypted, encKey)
		if err != nil {
			return nil, fmt.Errorf("decrypting finding %d: %w", f.ID, err)
		}
		f.KeyValue = string(plain)
		f.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
		findings = append(findings, f)
	}
	return findings, rows.Err()
}
