// Package audit records privileged write operations to an embedded SQLite
// database so there is a durable "who did what" trail.
package auditlog

import (
	"database/sql"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"sync"
	"time"

	_ "modernc.org/sqlite"
)

// buffer sizes the channel that decouples Record (request path) from the DB
// writer goroutine. Bursts up to this many entries return immediately; only a
// sustained overflow falls back to a synchronous write.
const buffer = 1024

// Entry is one recorded action.
type Entry struct {
	Time     string `json:"time" example:"2026-06-20T08:15:04Z" doc:"When the action occurred (RFC3339, UTC)"`
	Username string `json:"username" example:"alice" doc:"Who performed it"`
	Method   string `json:"method" example:"POST" doc:"HTTP method"`
	Path     string `json:"path" example:"/api/users" doc:"Request path"`
	Module   string `json:"module" example:"users" doc:"Target module"`
	Status   int    `json:"status" example:"200" doc:"HTTP response status"`

	// ts is the capture time (unix seconds), carried through the writer channel
	// so the stored timestamp reflects when the action happened, not when the
	// background writer got to it.
	ts int64
}

type Store struct {
	db   *sql.DB
	ch   chan Entry
	done chan struct{} // closed when the writer has drained and exited

	// mu guards the channel against a Record racing Close. Record takes RLock
	// (many concurrent senders); Close takes the write lock to flip closed and
	// close the channel exactly once. Without it, a Record after Close would
	// panic sending on a closed channel.
	mu     sync.RWMutex
	closed bool
}

// New opens (creating if needed) the audit database at path and starts the
// background writer that drains recorded entries to disk.
func New(path string) (*Store, error) {
	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
		return nil, fmt.Errorf("audit db dir: %w", err)
	}
	db, err := sql.Open("sqlite", path)
	if err != nil {
		return nil, fmt.Errorf("open audit db: %w", err)
	}
	// ponytail: single connection serializes writes, same rationale as the
	// session store; the background writer is the only writer.
	db.SetMaxOpenConns(1)
	if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS audit (
		id       INTEGER PRIMARY KEY AUTOINCREMENT,
		ts       INTEGER NOT NULL,
		username TEXT NOT NULL,
		method   TEXT NOT NULL,
		path     TEXT NOT NULL,
		module   TEXT NOT NULL,
		status   INTEGER NOT NULL
	)`); err != nil {
		return nil, fmt.Errorf("create audit table: %w", err)
	}
	s := &Store{db: db, ch: make(chan Entry, buffer), done: make(chan struct{})}
	go s.writer()
	return s, nil
}

// writer is the single goroutine that persists entries, keeping all DB writes
// off the request path. It exits once Close closes the channel, after draining
// whatever is buffered.
func (s *Store) writer() {
	defer close(s.done)
	for e := range s.ch {
		s.insert(e)
	}
}

// Close stops accepting entries, drains those still buffered, and closes the
// database. Call it during shutdown, after the HTTP server has stopped so no
// further Record calls can race with the channel close.
func (s *Store) Close() error {
	s.mu.Lock()
	s.closed = true
	close(s.ch)
	s.mu.Unlock()
	<-s.done
	return s.db.Close()
}

func (s *Store) insert(e Entry) {
	if _, err := s.db.Exec(
		`INSERT INTO audit (ts, username, method, path, module, status) VALUES (?, ?, ?, ?, ?, ?)`,
		e.ts, e.Username, e.Method, e.Path, e.Module, e.Status,
	); err != nil {
		log.Printf("audit: insert failed (%s %s by %s): %v", e.Method, e.Path, e.Username, err)
	}
}

// Record queues an entry for the background writer and returns immediately. If
// the buffer is full (sustained overload), it writes synchronously instead so an
// audit entry is never silently dropped - only then does it pay DB latency.
func (s *Store) Record(username, method, path, module string, status int) {
	e := Entry{ts: time.Now().Unix(), Username: username, Method: method, Path: path, Module: module, Status: status}
	s.mu.RLock()
	defer s.mu.RUnlock()
	if s.closed {
		s.insert(e) // post-shutdown straggler: write directly, channel is gone
		return
	}
	select {
	case s.ch <- e:
	default:
		s.insert(e)
	}
}

// List returns the most recent entries, newest first, capped at limit.
func (s *Store) List(limit int) ([]Entry, error) {
	rows, err := s.db.Query(
		`SELECT ts, username, method, path, module, status FROM audit ORDER BY id DESC LIMIT ?`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	entries := []Entry{}
	for rows.Next() {
		var e Entry
		var ts int64
		if err := rows.Scan(&ts, &e.Username, &e.Method, &e.Path, &e.Module, &e.Status); err != nil {
			return nil, err
		}
		e.Time = time.Unix(ts, 0).UTC().Format(time.RFC3339)
		entries = append(entries, e)
	}
	return entries, rows.Err()
}
