// sogoms-logs : Microservice de logging centralisé.
// Écrit les logs applicatifs dans des fichiers avec rotation automatique.
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"os"
	"os/signal"
	"path/filepath"
	"sync"
	"syscall"
	"time"

	"sogoms.com/internal/config"
	"sogoms.com/internal/protocol"
)

var (
	socketPath    = flag.String("socket", "/run/sogoms-logs.1.sock", "Unix socket path")
	configDir     = flag.String("config", "/config", "Configuration directory")
	logDir        = flag.String("logdir", "/var/log/sogoms", "Log files directory")
	retentionDays = flag.Int("retention", 30, "Default retention days")
)

// LogEntry représente une entrée de log au format JSON.
type LogEntry struct {
	Timestamp string         `json:"timestamp"`
	App       string         `json:"app"`
	Type      string         `json:"type"`
	Level     string         `json:"level,omitempty"`
	Message   string         `json:"message,omitempty"`
	EventType string         `json:"event_type,omitempty"`
	Data      map[string]any `json:"data,omitempty"`
	Context   map[string]any `json:"context,omitempty"`
}

// LogPool gère les fichiers de log par application.
type LogPool struct {
	registry      *config.Registry
	logDir        string
	retentionDays int
	files         map[string]*os.File // clé: "{app}-{date}-{type}"
	mu            sync.Mutex
	stopRotation  chan struct{}
}

func NewLogPool(registry *config.Registry, logDir string, retentionDays int) *LogPool {
	return &LogPool{
		registry:      registry,
		logDir:        logDir,
		retentionDays: retentionDays,
		files:         make(map[string]*os.File),
		stopRotation:  make(chan struct{}),
	}
}

// Start démarre la goroutine de rotation des logs.
func (p *LogPool) Start() {
	// Créer le répertoire de logs si nécessaire
	if err := os.MkdirAll(p.logDir, 0755); err != nil {
		log.Printf("[logs] warning: cannot create log dir: %v", err)
	}

	// Rotation initiale
	p.rotate()

	// Goroutine de rotation quotidienne
	go func() {
		ticker := time.NewTicker(24 * time.Hour)
		defer ticker.Stop()
		for {
			select {
			case <-ticker.C:
				p.rotate()
			case <-p.stopRotation:
				return
			}
		}
	}()
}

// rotate supprime les fichiers de log plus vieux que retentionDays.
func (p *LogPool) rotate() {
	cutoff := time.Now().AddDate(0, 0, -p.retentionDays)
	entries, err := os.ReadDir(p.logDir)
	if err != nil {
		log.Printf("[logs] rotation error: %v", err)
		return
	}

	deleted := 0
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		info, err := entry.Info()
		if err != nil {
			continue
		}
		if info.ModTime().Before(cutoff) {
			path := filepath.Join(p.logDir, entry.Name())
			if err := os.Remove(path); err == nil {
				deleted++
			}
		}
	}
	if deleted > 0 {
		log.Printf("[logs] rotation: deleted %d old files", deleted)
	}
}

// Write écrit une entrée de log dans le fichier approprié.
func (p *LogPool) Write(appID, logType string, entry *LogEntry) error {
	date := time.Now().Format("20060102")
	key := fmt.Sprintf("%s-%s-%s", appID, date, logType)
	filename := filepath.Join(p.logDir, key+".log")

	p.mu.Lock()
	defer p.mu.Unlock()

	// Obtenir ou créer le fichier
	f, ok := p.files[key]
	if !ok {
		var err error
		f, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
		if err != nil {
			return fmt.Errorf("open log file: %w", err)
		}
		p.files[key] = f
	}

	// Écrire l'entrée JSON
	data, err := json.Marshal(entry)
	if err != nil {
		return fmt.Errorf("marshal entry: %w", err)
	}
	data = append(data, '\n')

	if _, err := f.Write(data); err != nil {
		return fmt.Errorf("write entry: %w", err)
	}

	return nil
}

// Close ferme tous les fichiers ouverts.
func (p *LogPool) Close() {
	close(p.stopRotation)

	p.mu.Lock()
	defer p.mu.Unlock()

	for key, f := range p.files {
		f.Close()
		delete(p.files, key)
	}
	log.Printf("[logs] closed all log files")
}

func main() {
	flag.Parse()
	log.SetFlags(log.Ltime | log.Lshortfile)

	// Charger les configurations
	registry := config.NewRegistry(*configDir)
	if err := registry.Load(); err != nil {
		log.Fatalf("load config: %v", err)
	}
	log.Printf("[logs] loaded apps: %v", registry.Apps())

	// Pool de fichiers logs
	logPool := NewLogPool(registry, *logDir, *retentionDays)
	logPool.Start()
	defer logPool.Close()

	// Handler des requêtes
	handler := func(ctx context.Context, req *protocol.Request) *protocol.Response {
		return handleRequest(ctx, req, logPool)
	}

	// Démarrer le serveur
	server := protocol.NewServer(*socketPath, handler)
	if err := server.Start(); err != nil {
		log.Fatalf("start server: %v", err)
	}

	log.Printf("[logs] sogoms-logs started on %s", *socketPath)

	// Attendre signal d'arrêt
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
	<-sigCh

	log.Printf("[logs] shutting down...")
	server.Stop()
}

func handleRequest(ctx context.Context, req *protocol.Request, logPool *LogPool) *protocol.Response {
	switch req.Action {
	case "health":
		return protocol.Success(req.ID, map[string]any{"status": "ok"})
	case "log_error":
		return handleLogError(req, logPool)
	case "log_event":
		return handleLogEvent(req, logPool)
	default:
		return protocol.Failure(req.ID, "UNKNOWN_ACTION", "unknown action: "+req.Action)
	}
}

// handleLogError écrit un log d'erreur.
// Params: app_id, level (error|warn|info), message, context (optional)
func handleLogError(req *protocol.Request, logPool *LogPool) *protocol.Response {
	appID, ok := req.Params["app_id"].(string)
	if !ok || appID == "" {
		return protocol.Failure(req.ID, "MISSING_APP_ID", "app_id is required")
	}

	level, _ := req.Params["level"].(string)
	if level == "" {
		level = "error"
	}

	message, ok := req.Params["message"].(string)
	if !ok || message == "" {
		return protocol.Failure(req.ID, "MISSING_MESSAGE", "message is required")
	}

	var context map[string]any
	if ctx, ok := req.Params["context"].(map[string]any); ok {
		context = ctx
	}

	entry := &LogEntry{
		Timestamp: time.Now().Format(time.RFC3339),
		App:       appID,
		Type:      "error",
		Level:     level,
		Message:   message,
		Context:   context,
	}

	if err := logPool.Write(appID, "error", entry); err != nil {
		return protocol.Failure(req.ID, "WRITE_ERROR", err.Error())
	}

	return protocol.Success(req.ID, map[string]any{"logged": true})
}

// handleLogEvent écrit un log d'événement.
// Params: app_id, event_type, data
func handleLogEvent(req *protocol.Request, logPool *LogPool) *protocol.Response {
	appID, ok := req.Params["app_id"].(string)
	if !ok || appID == "" {
		return protocol.Failure(req.ID, "MISSING_APP_ID", "app_id is required")
	}

	eventType, ok := req.Params["event_type"].(string)
	if !ok || eventType == "" {
		return protocol.Failure(req.ID, "MISSING_EVENT_TYPE", "event_type is required")
	}

	var data map[string]any
	if d, ok := req.Params["data"].(map[string]any); ok {
		data = d
	}

	entry := &LogEntry{
		Timestamp: time.Now().Format(time.RFC3339),
		App:       appID,
		Type:      "event",
		EventType: eventType,
		Data:      data,
	}

	if err := logPool.Write(appID, "event", entry); err != nil {
		return protocol.Failure(req.ID, "WRITE_ERROR", err.Error())
	}

	return protocol.Success(req.ID, map[string]any{"logged": true})
}
