package providers

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/niels/banking-app/backend/internal/domain"
)

const (
	StatusSuccess     = "success"
	StatusError       = "error"
	StatusTimeout     = "timeout"
	StatusCircuitOpen = "circuit_open"
)

type Controls struct {
	Timeout                 time.Duration
	MaxAttempts             int
	RetryBaseDelay          time.Duration
	CircuitFailureThreshold int
	CircuitCooldown         time.Duration
	DegradedMode            string
	AuditEnabled            bool
}

type CallMeta struct {
	ProviderType   string
	ProviderName   string
	Operation      string
	BusinessKey    string
	IdempotencyKey string
}

type OutboundCallRecord struct {
	ProviderType   string
	ProviderName   string
	Operation      string
	IdempotencyKey string
	Status         string
	Attempts       int
	Duration       time.Duration
	Request        json.RawMessage
	Response       json.RawMessage
	ErrorCode      string
	ErrorMessage   string
	RequestHash    string
	DegradedMode   string
	CircuitState   string
	StartedAt      time.Time
	FinishedAt     time.Time
}

type OutboundCallRecorder interface {
	RecordOutboundCall(ctx context.Context, record OutboundCallRecord) error
}

type MetricsRecorder interface {
	RecordProviderCall(providerType, providerName, status string)
	SetProviderAvailability(providerType, providerName string, available bool)
}

type Executor struct {
	controls Controls
	recorder OutboundCallRecorder
	metrics  MetricsRecorder
	breakers *circuitBreakers
}

func NewExecutor(controls Controls, recorder OutboundCallRecorder, metrics MetricsRecorder) *Executor {
	controls = normalizeControls(controls)
	return &Executor{
		controls: controls,
		recorder: recorder,
		metrics:  metrics,
		breakers: newCircuitBreakers(controls.CircuitFailureThreshold, controls.CircuitCooldown),
	}
}

func Execute[T any](ctx context.Context, executor *Executor, meta CallMeta, request any, call func(context.Context) (T, error)) (T, error) {
	var zero T
	if executor == nil {
		return call(ctx)
	}
	meta = normalizeCallMeta(meta, request)
	key := meta.ProviderType + ":" + meta.ProviderName + ":" + meta.Operation
	startedAt := time.Now().UTC()
	requestJSON := RedactJSONValue(request)
	requestHash := fingerprint(request)
	attempts := 0
	status := StatusError
	circuitState := "closed"
	var responseJSON json.RawMessage = json.RawMessage(`{}`)
	var lastErr error

	if !executor.breakers.allow(key) {
		status = StatusCircuitOpen
		circuitState = "open"
		lastErr = fmt.Errorf("%w: %s %s circuit is open", domain.ErrProviderUnavailable, meta.ProviderName, meta.Operation)
		executor.record(ctx, OutboundCallRecord{
			ProviderType:   meta.ProviderType,
			ProviderName:   meta.ProviderName,
			Operation:      meta.Operation,
			IdempotencyKey: meta.IdempotencyKey,
			Status:         status,
			Attempts:       attempts,
			Duration:       time.Since(startedAt),
			Request:        requestJSON,
			Response:       responseJSON,
			ErrorCode:      "circuit_open",
			ErrorMessage:   lastErr.Error(),
			RequestHash:    requestHash,
			DegradedMode:   executor.controls.DegradedMode,
			CircuitState:   circuitState,
			StartedAt:      startedAt,
			FinishedAt:     time.Now().UTC(),
		})
		return zero, lastErr
	}

	for attempts < executor.controls.MaxAttempts {
		attempts++
		attemptCtx, cancel := context.WithTimeout(ctx, executor.controls.Timeout)
		result, err := call(attemptCtx)
		cancel()

		if err == nil {
			executor.breakers.record(key, true)
			status = StatusSuccess
			responseJSON = RedactJSONValue(result)
			executor.record(ctx, OutboundCallRecord{
				ProviderType:   meta.ProviderType,
				ProviderName:   meta.ProviderName,
				Operation:      meta.Operation,
				IdempotencyKey: meta.IdempotencyKey,
				Status:         status,
				Attempts:       attempts,
				Duration:       time.Since(startedAt),
				Request:        requestJSON,
				Response:       responseJSON,
				RequestHash:    requestHash,
				DegradedMode:   executor.controls.DegradedMode,
				CircuitState:   circuitState,
				StartedAt:      startedAt,
				FinishedAt:     time.Now().UTC(),
			})
			return result, nil
		}

		lastErr = err
		status = classifyStatus(err)
		if !retryableProviderError(err) || attempts >= executor.controls.MaxAttempts {
			break
		}
		select {
		case <-ctx.Done():
			lastErr = ctx.Err()
			status = classifyStatus(lastErr)
			attempts = executor.controls.MaxAttempts
		case <-time.After(executor.retryDelay(attempts)):
		}
	}

	executor.breakers.record(key, false)
	executor.record(ctx, OutboundCallRecord{
		ProviderType:   meta.ProviderType,
		ProviderName:   meta.ProviderName,
		Operation:      meta.Operation,
		IdempotencyKey: meta.IdempotencyKey,
		Status:         status,
		Attempts:       attempts,
		Duration:       time.Since(startedAt),
		Request:        requestJSON,
		Response:       responseJSON,
		ErrorCode:      providerErrorCode(lastErr),
		ErrorMessage:   lastErr.Error(),
		RequestHash:    requestHash,
		DegradedMode:   executor.controls.DegradedMode,
		CircuitState:   circuitState,
		StartedAt:      startedAt,
		FinishedAt:     time.Now().UTC(),
	})
	return zero, lastErr
}

func (e *Executor) record(ctx context.Context, record OutboundCallRecord) {
	if e.metrics != nil {
		e.metrics.RecordProviderCall(record.ProviderType, record.ProviderName, record.Status)
		e.metrics.SetProviderAvailability(record.ProviderType, record.ProviderName, record.Status == StatusSuccess)
	}
	if e.recorder == nil || !e.controls.AuditEnabled {
		return
	}
	if err := e.recorder.RecordOutboundCall(ctx, record); err != nil {
		// Provider audit recording must not turn a successful provider call into a customer failure.
		return
	}
}

func (e *Executor) retryDelay(attempt int) time.Duration {
	delay := time.Duration(attempt) * e.controls.RetryBaseDelay
	if delay > 5*time.Second {
		return 5 * time.Second
	}
	return delay
}

func normalizeControls(controls Controls) Controls {
	if controls.Timeout <= 0 {
		controls.Timeout = 3 * time.Second
	}
	if controls.MaxAttempts <= 0 {
		controls.MaxAttempts = 3
	}
	if controls.RetryBaseDelay <= 0 {
		controls.RetryBaseDelay = 100 * time.Millisecond
	}
	if controls.CircuitFailureThreshold <= 0 {
		controls.CircuitFailureThreshold = 5
	}
	if controls.CircuitCooldown <= 0 {
		controls.CircuitCooldown = time.Minute
	}
	if controls.DegradedMode == "" {
		controls.DegradedMode = "fail_closed"
	}
	return controls
}

func normalizeCallMeta(meta CallMeta, request any) CallMeta {
	if meta.ProviderType == "" {
		meta.ProviderType = "unknown"
	}
	if meta.ProviderName == "" {
		meta.ProviderName = meta.ProviderType
	}
	if meta.Operation == "" {
		meta.Operation = "call"
	}
	if meta.IdempotencyKey == "" {
		meta.IdempotencyKey = idempotencyKey(meta, request)
	}
	return meta
}

func idempotencyKey(meta CallMeta, request any) string {
	source := meta.ProviderType + "\x1f" + meta.ProviderName + "\x1f" + meta.Operation + "\x1f" + meta.BusinessKey + "\x1f" + fingerprint(request)
	sum := sha256.Sum256([]byte(source))
	return hex.EncodeToString(sum[:])
}

func fingerprint(value any) string {
	raw, err := json.Marshal(value)
	if err != nil {
		raw = []byte(fmt.Sprintf("%#v", value))
	}
	sum := sha256.Sum256(raw)
	return hex.EncodeToString(sum[:])
}

func classifyStatus(err error) string {
	if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
		return StatusTimeout
	}
	if errors.Is(err, domain.ErrProviderUnavailable) {
		return StatusError
	}
	return StatusError
}

func retryableProviderError(err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, domain.ErrValidation) ||
		errors.Is(err, domain.ErrInvalidCredentials) ||
		errors.Is(err, domain.ErrUnauthorized) ||
		errors.Is(err, domain.ErrForbidden) ||
		errors.Is(err, domain.ErrConflict) ||
		errors.Is(err, domain.ErrInsufficientFunds) ||
		errors.Is(err, domain.ErrLimitExceeded) {
		return false
	}
	return true
}

func providerErrorCode(err error) string {
	switch {
	case err == nil:
		return ""
	case errors.Is(err, context.DeadlineExceeded):
		return "timeout"
	case errors.Is(err, domain.ErrProviderUnavailable):
		return "provider_unavailable"
	case errors.Is(err, domain.ErrValidation):
		return "validation_failed"
	default:
		return "provider_error"
	}
}

type circuitBreakers struct {
	mu        sync.Mutex
	threshold int
	cooldown  time.Duration
	states    map[string]*circuitState
}

type circuitState struct {
	failures    int
	openedUntil time.Time
}

func newCircuitBreakers(threshold int, cooldown time.Duration) *circuitBreakers {
	return &circuitBreakers{
		threshold: threshold,
		cooldown:  cooldown,
		states:    map[string]*circuitState{},
	}
}

func (b *circuitBreakers) allow(key string) bool {
	b.mu.Lock()
	defer b.mu.Unlock()

	state := b.states[key]
	if state == nil {
		return true
	}
	if state.openedUntil.IsZero() || time.Now().UTC().After(state.openedUntil) {
		return true
	}
	return false
}

func (b *circuitBreakers) record(key string, success bool) {
	b.mu.Lock()
	defer b.mu.Unlock()

	state := b.states[key]
	if state == nil {
		state = &circuitState{}
		b.states[key] = state
	}
	if success {
		state.failures = 0
		state.openedUntil = time.Time{}
		return
	}
	state.failures++
	if state.failures >= b.threshold {
		state.openedUntil = time.Now().UTC().Add(b.cooldown)
	}
}
