// Package audit provides an audit event writer for compliance and review.
// Events are written to a JSONL file and/or sent to a Matrix room.
// This is fully impure (I/O): belongs in shell/.
package audit

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/enmanuel/agents/internal/config"
)

// Event types emitted by the audit trail.
const (
	EventMessageReceived = "message_received"
	EventCommandExec     = "command_exec"
	EventToolExec        = "tool_exec"
	EventLLMRequest      = "llm_request"
	EventLLMError        = "llm_error"
)

// Event represents a single audit trail entry.
type Event struct {
	Time      time.Time `json:"time"`
	AgentID   string    `json:"agent_id"`
	EventType string    `json:"event_type"`
	SenderID  string    `json:"sender_id,omitempty"`
	RoomID    string    `json:"room_id,omitempty"`
	Detail    string    `json:"detail,omitempty"`
}

// MatrixSender is a function that sends a message to a Matrix room.
// Decouples audit from the Matrix client.
type MatrixSender func(roomID, msg string)

// Writer writes audit events to a JSONL file and/or a Matrix room.
type Writer struct {
	cfg    config.AuditCfg
	sender MatrixSender // may be nil
	logger *slog.Logger

	include map[string]bool // allowlist of event types; empty = all

	mu   sync.Mutex
	file *os.File
}

// New creates an AuditWriter from the given config.
// matrixSender may be nil if LogToRoom is not configured.
func New(cfg config.AuditCfg, sender MatrixSender, logger *slog.Logger) (*Writer, error) {
	w := &Writer{
		cfg:    cfg,
		sender: sender,
		logger: logger.With("component", "audit"),
	}

	// Build include allowlist
	if len(cfg.Include) > 0 {
		w.include = make(map[string]bool, len(cfg.Include))
		for _, t := range cfg.Include {
			w.include[t] = true
		}
	}

	// Open log file if configured
	if cfg.LogFile != "" {
		dir := filepath.Dir(cfg.LogFile)
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return nil, fmt.Errorf("create audit log dir %s: %w", dir, err)
		}
		f, err := os.OpenFile(cfg.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
		if err != nil {
			return nil, fmt.Errorf("open audit log %s: %w", cfg.LogFile, err)
		}
		w.file = f
	}

	return w, nil
}

// Emit writes an audit event. If the event type is not in the include list
// (when non-empty), the event is silently dropped. Thread-safe.
func (w *Writer) Emit(evt Event) {
	// Filter by include allowlist (empty = pass all)
	if len(w.include) > 0 && !w.include[evt.EventType] {
		return
	}

	// Ensure time is set
	if evt.Time.IsZero() {
		evt.Time = time.Now().UTC()
	}

	// Write to JSONL file
	if w.file != nil {
		data, err := json.Marshal(evt)
		if err != nil {
			w.logger.Error("audit_marshal_error", "err", err)
			return
		}
		data = append(data, '\n')

		w.mu.Lock()
		_, writeErr := w.file.Write(data)
		w.mu.Unlock()

		if writeErr != nil {
			w.logger.Error("audit_write_error", "err", writeErr)
		}
	}

	// Send to Matrix room
	if w.sender != nil && w.cfg.LogToRoom != "" {
		msg := fmt.Sprintf("**[audit]** `%s` | agent=%s sender=%s room=%s | %s",
			evt.EventType, evt.AgentID, evt.SenderID, evt.RoomID, evt.Detail)
		w.sender(w.cfg.LogToRoom, msg)
	}
}

// Close closes the underlying log file.
func (w *Writer) Close() error {
	w.mu.Lock()
	defer w.mu.Unlock()
	if w.file != nil {
		return w.file.Close()
	}
	return nil
}
