package services

import (
	"bytes"
	"log"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/mrhid6/vantage/server/internal/db"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
)

func WorkflowLogDir() string {
	dir := os.Getenv("VANTAGE_WORKFLOW_LOG_DIR")
	if dir == "" {
		dir = filepath.Join("data", "workflow-logs")
	}
	_ = os.MkdirAll(dir, 0700)
	return dir
}

func ServerRunLogPath(runID, serverID string) string {
	return filepath.Join(WorkflowLogDir(), runID, serverID+".log")
}

func logTS() string {
	return time.Now().UTC().Format("2006-01-02T15:04:05.000") + "Z"
}

func AppendMarker(runID, serverID, text string) (int64, error) {
	path := ServerRunLogPath(runID, serverID)
	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
		return 0, err
	}
	f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
	if err != nil {
		return 0, err
	}
	defer f.Close()
	off, _ := f.Seek(0, 2)
	if _, err := f.WriteString("[" + logTS() + "] " + text + "\n"); err != nil {
		return off, err
	}
	return off, nil
}

type stepLogWriter struct {
	mu      sync.Mutex
	f       *os.File
	carry   []byte
	secrets []string
}

type stepLogRegistry struct {
	mu      sync.Mutex
	writers map[string]*stepLogWriter
}

var StepLogs = &stepLogRegistry{writers: make(map[string]*stepLogWriter)}

func (r *stepLogRegistry) Open(commandID, path string, secrets []string) error {
	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
		return err
	}
	f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
	if err != nil {
		return err
	}
	w := &stepLogWriter{f: f, secrets: secrets}
	r.mu.Lock()
	r.writers[commandID] = w
	r.mu.Unlock()
	return nil
}

func (r *stepLogRegistry) get(commandID string) *stepLogWriter {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.writers[commandID]
}

func (r *stepLogRegistry) Append(commandID string, data []byte) {
	w := r.get(commandID)
	if w == nil {
		return
	}
	w.mu.Lock()
	defer w.mu.Unlock()
	buf := append(w.carry, data...)
	for {
		i := bytes.IndexByte(buf, '\n')
		if i < 0 {
			break
		}
		w.writeLine(buf[:i])
		buf = buf[i+1:]
	}
	w.carry = append([]byte{}, buf...)
}

func (w *stepLogWriter) writeLine(line []byte) {
	masked := maskBytes(line, w.secrets)
	_, _ = w.f.WriteString("[" + logTS() + "] ")
	_, _ = w.f.Write(masked)
	_, _ = w.f.WriteString("\n")
}

func (r *stepLogRegistry) Close(commandID string) {
	r.mu.Lock()
	w := r.writers[commandID]
	delete(r.writers, commandID)
	r.mu.Unlock()
	if w == nil {
		return
	}
	w.mu.Lock()
	defer w.mu.Unlock()
	if len(w.carry) > 0 {
		w.writeLine(w.carry)
		w.carry = nil
	}
	_ = w.f.Close()
}

func maskBytes(b []byte, secrets []string) []byte {
	s := string(b)
	for _, v := range secrets {
		if v == "" {
			continue
		}
		s = strings.ReplaceAll(s, v, "***")
	}
	return []byte(s)
}

func StartLogSweeper() {
	go func() {
		sweepLogs()
		t := time.NewTicker(time.Hour)
		defer t.Stop()
		for range t.C {
			sweepLogs()
		}
	}()
}

func sweepLogs() {
	base := WorkflowLogDir()
	entries, err := os.ReadDir(base)
	if err != nil {
		return
	}
	cache := map[string]int{}
	now := time.Now()

	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		runID := e.Name()
		dir := filepath.Join(base, runID)

		instanceID, finishedAt, found, err := runRetentionInfo(runID)
		if err != nil {

			log.Printf("log sweep: retention lookup failed for run %s: %v", runID, err)
			continue
		}
		if found && finishedAt == nil {
			continue
		}

		days, ok := cache[instanceID]
		if !ok {
			days = defaultRetentionDays
			if instanceID != "" {
				if v, err := GetWorkflowLogRetentionDays(instanceID); err == nil {
					days = v
				}
			}
			cache[instanceID] = days
		}
		if days <= 0 {
			continue
		}
		cutoff := now.AddDate(0, 0, -days)

		if found {
			if finishedAt.Before(cutoff) {
				_ = os.RemoveAll(dir)
			}
			continue
		}

		if fi, e := os.Stat(dir); e == nil && fi.ModTime().Before(cutoff) {
			_ = os.RemoveAll(dir)
		}
	}
}

const defaultRetentionDays = 30

func runRetentionInfo(runID string) (string, *time.Time, bool, error) {
	ctx, cancel := wfCtx()
	defer cancel()
	var run struct {
		InstanceID string     `bson:"instance_id"`
		FinishedAt *time.Time `bson:"finished_at"`
	}
	err := db.Col("workflow_runs").FindOne(ctx, bson.M{"run_id": runID}).Decode(&run)
	if err == mongo.ErrNoDocuments {
		return "", nil, false, nil
	}
	if err != nil {
		return "", nil, false, err
	}
	return run.InstanceID, run.FinishedAt, true, nil
}
