// Package dependencyretry keeps recoverable dependency failures inside their
// long-lived owner. Callers must retry a read or a durable, idempotent operation
// from its entry point, never a captured external side effect or transaction.
package dependencyretry

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"syscall"
	"time"

	"github.com/go-sql-driver/mysql"
	commonv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

// Do retries only explicitly transient failures, with 250ms–5s backoff. A
// prolonged outage is not a reason to kill the owner. Its parent context owns
// shutdown/deadline; successful return resets backoff for the next operation.
func Do(ctx context.Context, operation string, attempt func() error) error {
	return do(ctx, operation, attempt, 250*time.Millisecond, 5*time.Second)
}

func do(ctx context.Context, operation string, attempt func() error, delay, maximum time.Duration) error {
	for {
		if err := ctx.Err(); err != nil {
			return err
		}
		err := attempt()
		if parentErr := ctx.Err(); parentErr != nil {
			return parentErr
		}
		if err == nil {
			return nil
		}
		reason := transientReason(err)
		if reason == "" {
			return fmt.Errorf("%s: %w", operation, err)
		}
		// Only static operation/reason codes are logged, never SQL values,
		// provider bodies, credential material or arbitrary dependency errors.
		log.Printf("connector dependency recovery operation=%q reason=%s retry_delay=%s", operation, reason, delay)
		timer := time.NewTimer(delay)
		select {
		case <-ctx.Done():
			timer.Stop()
			return ctx.Err()
		case <-timer.C:
		}
		delay = min(delay*2, maximum)
	}
}

func transientReason(err error) string {
	if err == nil {
		return ""
	}
	if joined, ok := err.(interface{ Unwrap() []error }); ok {
		reason := ""
		for _, child := range joined.Unwrap() {
			// A canceled database/sql transaction may already have rolled back.
			// This cleanup result adds no independent cause; alone it is not retryable.
			if child == nil || child == sql.ErrTxDone {
				continue
			}
			if transientReason(child) == "" {
				return ""
			}
			reason = "transient_dependencies"
		}
		return reason
	}
	switch failure := err.(type) {
	case *commonv1.Failure:
		switch failure.Category {
		case commonv1.FailureUnavailable, commonv1.FailureDeadlineExceeded, commonv1.FailureResourceExhausted:
			return string(failure.Category)
		}
		return ""
	case *mysql.MySQLError:
		switch failure.Number {
		case 1040, 1205, 1213, 2006, 2013, 3572:
			return fmt.Sprintf("database_%d", failure.Number)
		}
		return ""
	}
	if unwrapped := errors.Unwrap(err); unwrapped != nil {
		return transientReason(unwrapped)
	}
	switch err {
	case context.DeadlineExceeded:
		return "dependency_deadline"
	case driver.ErrBadConn, mysql.ErrInvalidConn, sql.ErrConnDone:
		return "database_connection"
	case io.EOF, io.ErrUnexpectedEOF, syscall.ECONNRESET, syscall.ECONNREFUSED, syscall.EPIPE, syscall.ETIMEDOUT:
		return "transport_connection"
	}
	if failure, ok := err.(net.Error); ok && failure.Timeout() {
		return "transport_timeout"
	}
	return ""
}
