package storage

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

// Filters selects a subset of findings for ListFindingsFiltered.
// Empty Provider means "any provider". Nil Verified means "any verified state".
// Limit <= 0 disables pagination (Offset is then ignored).
type Filters struct {
	Provider string
	Verified *bool
	Limit    int
	Offset   int
}

// ListFindingsFiltered returns findings matching the given filters, newest first.
// Key values are decrypted before return. encKey must match the key used at save time.
func (db *DB) ListFindingsFiltered(encKey []byte, f Filters) ([]Finding, error) {
	var (
		where []string
		args  []interface{}
	)
	if f.Provider != "" {
		where = append(where, "provider_name = ?")
		args = append(args, f.Provider)
	}
	if f.Verified != nil {
		where = append(where, "verified = ?")
		if *f.Verified {
			args = append(args, 1)
		} else {
			args = append(args, 0)
		}
	}
	q := `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`
	if len(where) > 0 {
		q += " WHERE " + strings.Join(where, " AND ")
	}
	q += " ORDER BY created_at DESC, id DESC"
	if f.Limit > 0 {
		q += " LIMIT ? OFFSET ?"
		args = append(args, f.Limit, f.Offset)
	}

	rows, err := db.sql.Query(q, args...)
	if err != nil {
		return nil, fmt.Errorf("querying findings: %w", err)
	}
	defer rows.Close()

	var out []Finding
	for rows.Next() {
		finding, err := scanFindingRow(rows, encKey)
		if err != nil {
			return nil, err
		}
		out = append(out, finding)
	}
	return out, rows.Err()
}

// GetFinding returns a single finding by id. Returns sql.ErrNoRows when no
// finding with the given id exists; callers can detect this with errors.Is.
func (db *DB) GetFinding(id int64, encKey []byte) (*Finding, error) {
	row := db.sql.QueryRow(
		`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 WHERE id = ?`, id)
	f, err := scanFindingRowFromRow(row, encKey)
	if err != nil {
		return nil, err
	}
	return &f, nil
}

// FindingExistsByKey reports whether a finding with the same provider name,
// masked key, source path, and line number already exists in the database.
// This is the identity tuple used by the import pipeline to make repeated
// imports of the same scanner output idempotent without decrypting stored
// key values.
func (db *DB) FindingExistsByKey(provider, masked, sourcePath string, line int) (bool, error) {
	row := db.sql.QueryRow(
		`SELECT 1 FROM findings
		 WHERE provider_name = ? AND key_masked = ? AND source_path = ? AND line_number = ?
		 LIMIT 1`,
		provider, masked, sourcePath, line,
	)
	var one int
	if err := row.Scan(&one); err != nil {
		if err == sql.ErrNoRows {
			return false, nil
		}
		return false, fmt.Errorf("querying finding existence: %w", err)
	}
	return true, nil
}

// DeleteFinding removes the finding with the given id.
// Returns the number of rows affected (0 if no such id). A missing id is not
// an error — the caller decides whether to surface it.
func (db *DB) DeleteFinding(id int64) (int64, error) {
	res, err := db.sql.Exec(`DELETE FROM findings WHERE id = ?`, id)
	if err != nil {
		return 0, fmt.Errorf("deleting finding %d: %w", id, err)
	}
	return res.RowsAffected()
}

// scanFindingRow reads one Finding from *sql.Rows and decrypts its key value.
func scanFindingRow(rows *sql.Rows, encKey []byte) (Finding, error) {
	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 f, fmt.Errorf("scanning finding row: %w", err)
	}
	return hydrateFinding(f, encrypted, scanID, verifiedInt, metaJSON, createdAt, encKey)
}

// scanFindingRowFromRow reads one Finding from a *sql.Row. Propagates
// sql.ErrNoRows unchanged so callers can use errors.Is to detect a miss.
func scanFindingRowFromRow(row *sql.Row, encKey []byte) (Finding, error) {
	var f Finding
	var encrypted []byte
	var createdAt string
	var scanID sql.NullInt64
	var verifiedInt int
	var metaJSON sql.NullString
	if err := row.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 f, err // includes sql.ErrNoRows — let caller detect
	}
	return hydrateFinding(f, encrypted, scanID, verifiedInt, metaJSON, createdAt, encKey)
}

// hydrateFinding decrypts the key value and fills derived fields.
func hydrateFinding(f Finding, encrypted []byte, scanID sql.NullInt64, verifiedInt int, metaJSON sql.NullString, createdAt string, encKey []byte) (Finding, error) {
	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 f, fmt.Errorf("unmarshaling verify metadata for finding %d: %w", f.ID, err)
		}
		f.VerifyMetadata = m
	}
	plain, err := Decrypt(encrypted, encKey)
	if err != nil {
		return f, fmt.Errorf("decrypting finding %d: %w", f.ID, err)
	}
	f.KeyValue = string(plain)
	f.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
	return f, nil
}
