package migration

import (
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/tidbcloud/observability/vm-migration/internal/jsonio"
	"github.com/tidbcloud/observability/vm-migration/internal/schema"
	"github.com/tidbcloud/observability/vm-migration/internal/status"
)

// tenantActionLog persists detailed action output and optionally mirrors it to
// a live observer. Observability failures are reported but never change an
// action's business result.
type tenantActionLog struct {
	tenant   string
	path     string
	durable  io.WriteCloser
	observer io.WriteCloser
	warn     func(string)

	durableErrOnce  sync.Once
	observerErrOnce sync.Once
}

// openTenantActionLog opens the plan's durable per-tenant log and optional
// live observer. Either target may be unavailable without preventing the
// tenant from running.
func openTenantActionLog(
	dir string,
	tp schema.MigrationLoopTenant,
	observerFactory ActionLogObserver,
	warn func(string),
) *tenantActionLog {
	logFile := tp.LogFile
	if logFile == "" {
		logFile = tp.Name + ".log"
	}
	path := filepath.Join(dir, logFile)
	l := &tenantActionLog{tenant: tp.Name, path: path, warn: warn}

	f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
	if err != nil {
		l.reportDurable("open", err)
	} else {
		l.durable = f
	}
	if observerFactory != nil {
		l.observer = observerFactory(tp.Name)
	}
	return l
}

// Path returns the durable log path, including when the file could not be
// opened, so progress messages can direct operators to the intended artifact.
func (l *tenantActionLog) Path() string { return l.path }

func (l *tenantActionLog) Write(p []byte) (int, error) {
	if l.durable != nil {
		if err := jsonio.WriteFull(l.durable, p); err != nil {
			l.reportDurable("write", err)
		}
	}
	if l.observer != nil {
		if err := jsonio.WriteFull(l.observer, p); err != nil {
			l.reportObserver("write", err)
		}
	}
	return len(p), nil
}

// Close releases both targets. Close failures are warnings and do not replace
// the migration result.
func (l *tenantActionLog) Close() {
	if l.durable != nil {
		if err := l.durable.Close(); err != nil {
			l.reportDurable("close", err)
		}
	}
	if l.observer != nil {
		if err := l.observer.Close(); err != nil {
			l.reportObserver("close", err)
		}
	}
}

func (l *tenantActionLog) reportDurable(operation string, err error) {
	l.durableErrOnce.Do(func() {
		message := fmt.Sprintf("tenant=%s action log unavailable path=%s", l.tenant, l.path)
		if l.warn != nil {
			l.warn(message)
		}
		slog.Error("tenant action log failure",
			"tenant", l.tenant, "path", l.path, "operation", operation, "err", err)
	})
}

func (l *tenantActionLog) reportObserver(operation string, err error) {
	l.observerErrOnce.Do(func() {
		message := fmt.Sprintf("tenant=%s live action log unavailable", l.tenant)
		if l.warn != nil {
			l.warn(message)
		}
		slog.Error("tenant action log observer failure",
			"tenant", l.tenant, "operation", operation, "err", err)
	})
}

func writeStepHeader(actionLog *tenantActionLog, tenantName, stepName string, attempt int) {
	now := status.FormatTime(status.UTCNow())
	sep := strings.Repeat("-", 80)
	header := fmt.Sprintf("%s\n[%s] tenant=%s step=%s attempt=%d status=%s\n[%s] output:\n",
		sep, now, tenantName, stepName, attempt, StatusRunning, now)
	_, _ = io.WriteString(actionLog, header)
}

func writeStepResult(actionLog *tenantActionLog, statusStr string, exitCode int, errCat *schema.MigrationLoopError) {
	now := status.FormatTime(status.UTCNow())
	errStr := ""
	if errCat != nil {
		errStr = fmt.Sprintf(" error=%s", errCat.Category)
	}
	sep := strings.Repeat("-", 80)
	out := fmt.Sprintf("[%s] result: status=%s exitCode=%d%s\n%s\n", now, statusStr, exitCode, errStr, sep)
	_, _ = io.WriteString(actionLog, out)
}
