package store

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"time"
)

type AuditEntry struct {
	ID        int64          `json:"id"`
	Timestamp time.Time      `json:"timestamp"`
	Actor     string         `json:"actor"`
	Action    string         `json:"action"`
	Resource  string         `json:"resource"`
	Result    string         `json:"result"`
	Error     string         `json:"error,omitempty"`
	Metadata  map[string]any `json:"metadata,omitempty"`
}

type AuditRepo struct {
	db *sql.DB
}

func NewAuditRepo(db *sql.DB) *AuditRepo {
	return &AuditRepo{db: db}
}

// computeEntryHash computes sha256(prev_hash || timestamp || actor ||
// action || resource || result || error || metadata) for the hash
// chain (REQ-125, F2). The prev_hash is the entry_hash of the most
// recent prior entry (empty string for the first entry).
func computeEntryHash(prevHash, timestamp, actor, action, resource, result, errMsg, metaJSON string) string {
	h := sha256.New()
	h.Write([]byte(prevHash))
	h.Write([]byte{0})
	h.Write([]byte(timestamp))
	h.Write([]byte{0})
	h.Write([]byte(actor))
	h.Write([]byte{0})
	h.Write([]byte(action))
	h.Write([]byte{0})
	h.Write([]byte(resource))
	h.Write([]byte{0})
	h.Write([]byte(result))
	h.Write([]byte{0})
	h.Write([]byte(errMsg))
	h.Write([]byte{0})
	h.Write([]byte(metaJSON))
	return hex.EncodeToString(h.Sum(nil))
}

// getLastEntryHash returns the entry_hash of the most recent audit_log
// entry, or "" if the table is empty.
func (r *AuditRepo) getLastEntryHash(ctx context.Context) (string, error) {
	var prevHash string
	err := r.db.QueryRowContext(ctx,
		`SELECT entry_hash FROM audit_log ORDER BY id DESC LIMIT 1`).Scan(&prevHash)
	if err == sql.ErrNoRows {
		return "", nil
	}
	if err != nil {
		return "", fmt.Errorf("get last entry hash: %w", err)
	}
	return prevHash, nil
}

func (r *AuditRepo) Append(ctx context.Context, e *AuditEntry) error {
	if e.Timestamp.IsZero() {
		e.Timestamp = time.Now().UTC()
	}
	if e.Actor == "" {
		e.Actor = "system"
	}
	metaJSON, _ := json.Marshal(e.Metadata)
	tsStr := e.Timestamp.UTC().Format(time.RFC3339Nano)

	// Compute the hash chain (REQ-125, F2).
	prevHash, err := r.getLastEntryHash(ctx)
	if err != nil {
		return fmt.Errorf("audit hash chain: %w", err)
	}
	entryHash := computeEntryHash(prevHash, tsStr, e.Actor, e.Action, e.Resource, e.Result, e.Error, string(metaJSON))

	_, err = r.db.ExecContext(ctx,
		`INSERT INTO audit_log (timestamp, actor, action, resource, result, error, metadata, prev_hash, entry_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		e.Timestamp, e.Actor, e.Action, e.Resource, e.Result, e.Error, string(metaJSON), prevHash, entryHash)
	if err != nil {
		return fmt.Errorf("insert audit: %w", err)
	}
	return nil
}

// VerifyChain recomputes the hash chain from the first entry and
// returns an error if any entry's entry_hash does not match. Used by
// `orca doctor audit` (REQ-125).
func (r *AuditRepo) VerifyChain(ctx context.Context) error {
	rows, err := r.db.QueryContext(ctx,
		`SELECT id, timestamp, actor, action, resource, result, COALESCE(error, ''), COALESCE(metadata, ''), prev_hash, entry_hash FROM audit_log ORDER BY id ASC`)
	if err != nil {
		return fmt.Errorf("verify chain: query: %w", err)
	}
	defer rows.Close()
	prevHash := ""
	for rows.Next() {
		var (
			id         int64
			ts         time.Time
			actor      string
			action     string
			resource   string
			result     string
			errMsg     string
			metaJSON   string
			storedPrev string
			storedHash string
		)
		if err := rows.Scan(&id, &ts, &actor, &action, &resource, &result, &errMsg, &metaJSON, &storedPrev, &storedHash); err != nil {
			return fmt.Errorf("verify chain: scan: %w", err)
		}
		// Verify the prev_hash link.
		if storedPrev != prevHash {
			return fmt.Errorf("verify chain: entry %d prev_hash mismatch (expected %q, got %q)", id, prevHash, storedPrev)
		}
		// Recompute the entry hash.
		expected := computeEntryHash(prevHash, ts.UTC().Format(time.RFC3339Nano), actor, action, resource, result, errMsg, metaJSON)
		if expected != storedHash {
			return fmt.Errorf("verify chain: entry %d hash mismatch (entry may have been tampered)", id)
		}
		prevHash = storedHash
	}
	return rows.Err()
}

func (r *AuditRepo) List(ctx context.Context, limit int) ([]*AuditEntry, error) {
	if limit <= 0 {
		limit = 100
	}
	rows, err := r.db.QueryContext(ctx,
		`SELECT id, timestamp, actor, action, resource, result, COALESCE(error, ''), COALESCE(metadata, '') FROM audit_log ORDER BY id DESC LIMIT ?`, limit)
	if err != nil {
		return nil, fmt.Errorf("list audit: %w", err)
	}
	defer rows.Close()
	var entries []*AuditEntry
	for rows.Next() {
		var (
			e        AuditEntry
			metaJSON string
		)
		if err := rows.Scan(&e.ID, &e.Timestamp, &e.Actor, &e.Action, &e.Resource, &e.Result, &e.Error, &metaJSON); err != nil {
			return nil, fmt.Errorf("scan audit: %w", err)
		}
		if metaJSON != "" {
			_ = json.Unmarshal([]byte(metaJSON), &e.Metadata)
		}
		entries = append(entries, &e)
	}
	return entries, rows.Err()
}
