// Package audit "her işlem bir kayıt bırakır" ilkesini uygular.
//
// Neden ayrı paket: denetim kaydı normal uygulama loglarından FARKLIDIR ve
//   - sorgulanabilir/ayrı dosyada tutulmalı (soruşturma, uyumluluk),
//   - yapılandırılmış ve değişmez (append-only) olmalı,
//   - kaybolursa fark edilmelidir (yutulan hata olmamalı).
//
// Tasarım kararları:
//   - Kayıt JSON satırı olarak yazılır (bir satır = bir işlem): makine okur, insan da okur.
//   - Yazma senkron ve HATA VERMEZ: denetim kaydı üretim akışını durdurmaz. Hata durumunda
//     kayıp sessizce olmaz — std log'a düşer (yutma ilkesi).
//   - Yazma asla bloklamaz olmaya çalışır ama dosya hatası yutulmaz; çağıran etkilenmez.
package audit

import (
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"os"
	"strings"
	"sync"
	"time"

	"fiko-ai/pkg/id"
)

// Entry tek bir denetim kaydıdır. Alan adları bilinçli olarak düz tutuldu:
// kayıtlar sorgulanır/aktarılır, iç yapı değişebilir ama şema okunabilir kalmalı.
type Entry struct {
	ID             string         `json:"id"`
	At             time.Time      `json:"at"`
	Actor          string         `json:"actor"` // "user" | "agent" | "tool" | "system"
	UserID         string         `json:"userId,omitempty"`
	ConversationID string         `json:"conversationId,omitempty"`
	Action         string         `json:"action"` // "conversation.create", "message.send", ...
	Target         string         `json:"target,omitempty"`
	Result         string         `json:"result"` // "success" | "error" | "denied"
	Detail         string         `json:"detail,omitempty"`
	Meta           map[string]any `json:"meta,omitempty"`
}

// Sonuç değerleri.
const (
	ResultSuccess = "success"
	ResultError   = "error"
	ResultDenied  = "denied"
)

// Logger denetim kayıtlarını bir çıktıya yazar.
type Logger struct {
	mu     sync.Mutex
	w      io.Writer
	closer io.Closer // varsa (dosya), Close'ta kapatılır
	log    *slog.Logger
	now    func() time.Time
	seq    int
}

// Options Logger ayarlarıdır.
type Options struct {
	// Path dosya yolu; boşsa Stdout'a yazılır (konteyner ortamı için uygun).
	Path string
	// Log dosya hatası için; nil ise slog.Default kullanılır.
	Logger *slog.Logger
	// Clock zaman kaynağı (testte sahte saat verilebilir).
	Clock func() time.Time
}

// New Logger'ı oluşturur. Path verilmişse dosya açılır (append).
// Dosya açılamazsa hata döner — sessizce stdout'a düşmek yanıltıcı olurdu.
func New(opts Options) (*Logger, error) {
	l := &Logger{w: os.Stdout, log: opts.Logger, now: opts.Clock}
	if l.log == nil {
		l.log = slog.Default()
	}
	// Saat kaynağı verilmemişse varsayılanı KUR: Record l.now() çağırıyor,
	// nil saat kaynağı panic verirdi (sunucu açılışta çökerdi).
	if l.now == nil {
		l.now = time.Now
	}
	if opts.Clock != nil {
		l.now = opts.Clock
	}
	if opts.Path == "" {
		return l, nil
	}
	f, err := os.OpenFile(opts.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
	if err != nil {
		return nil, fmt.Errorf("audit: dosya açılamadı %q: %w", opts.Path, err)
	}
	// 0600: denetim kaydı hassas olabilir (kimlik, içerik özeti) — herkese açık olmasın.
	l.w = f
	l.closer = f
	return l, nil
}

// Record bir işlemi kaydeder. Dönüş hatası YOKTUR: denetim kaydı akışı durdurmaz.
// Yazma hatası olursa slog'a düşer (sessiz kayıp olmaz).
func (l *Logger) Record(e Entry) {
	if l == nil {
		return // Logger yoksa uygulama çalışmaya devam eder
	}
	l.seq++
	if e.ID == "" {
		e.ID = id.New("aud")
	}
	if e.At.IsZero() {
		e.At = l.now().UTC()
	}
	if e.Result == "" {
		e.Result = ResultSuccess
	}
	if e.Actor == "" {
		e.Actor = "system"
	}

	line, err := json.Marshal(e)
	if err != nil {
		l.log.Error("audit kaydı serileştirilemedi", "action", e.Action, "err", err)
		return
	}

	l.mu.Lock()
	defer l.mu.Unlock()
	if _, err := l.w.Write(append(line, '\n')); err != nil {
		// Yazma başarısız: kayıp olacak, ama üretim akışı DURMAZ.
		l.log.Error("audit kaydı yazılamadı", "action", e.Action, "err", err)
	}
}

// Close dosyayı kapatır (yoksa no-op).
func (l *Logger) Close() error {
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.closer != nil {
		err := l.closer.Close()
		l.closer = nil
		l.w = io.Discard // sonraki kayıtlar sessizce kaybolmasın diye discard: log zaten hata verdi
		return err
	}
	return nil
}

// ---- kolaylık yardımcıları ----

// Success başarılı bir kullanıcı işlemi kaydeder.
func (l *Logger) Success(userID, conversationID, action, detail string) {
	l.Record(Entry{Actor: "user", UserID: userID, ConversationID: conversationID, Action: action, Result: ResultSuccess, Detail: detail})
}

// Failure hatalı bir işlemi kaydeder.
func (l *Logger) Failure(userID, conversationID, action, detail string) {
	l.Record(Entry{Actor: "user", UserID: userID, ConversationID: conversationID, Action: action, Result: ResultError, Detail: detail})
}

// Denied yetki reddi kaydeder (spec: permission sistemi).
func (l *Logger) Denied(userID, action, detail string) {
	l.Record(Entry{Actor: "system", UserID: userID, Action: action, Result: ResultDenied, Detail: detail})
}

// Sanitize metinden denetim kaydına girmemesi gereken parçaları temizler.
// Kullanıcı içeriği denetim kaydına **özet olarak** gider, tam metin olarak DEĞİL
// (içerik zaten konuşma geçmişinde durur; audit iki kere saklamamalı).
//
// Kesme RUNE tabanlıdır, bayt değil: "çok uzun bir metin" gibi Türkçe metinlerde
// baytla kesilirse geçersiz UTF-8 üretilir ve kayıt okunamaz hale gelir.
func Sanitize(s string, max int) string {
	s = strings.TrimSpace(strings.ReplaceAll(s, "\n", " "))
	if max <= 0 || len(s) <= max {
		return s
	}
	const ellipsis = "..."
	runes := []rune(s)
	// Son harfleri kesmemek için: ayırıcı sığmıyorsa düz kes.
	keep := max - len(ellipsis)
	if keep < 0 {
		keep = 0
	}
	if keep > len(runes) {
		keep = len(runes)
	}
	return string(runes[:keep]) + ellipsis
}
