package impl

import (
	"strings"

	"github.com/tidbcloud/dataflow-service/app/repo/entity"
	encryptor_wrapper "github.com/tidbcloud/dataflow-service/pkg/encryptor-wrapper"
	"github.com/tidbcloud/pkgv2/bizerr"
)

// encryptedAliyunBucketURLPrefix marks bucket_url values encrypted for DB
// storage. Caller-provided values with this prefix are still plaintext at the
// write boundary and must be encrypted again.
const encryptedAliyunBucketURLPrefix = "encrypted:aliyun-bucket-url:v1:"

type auditLogSecretScope int

const (
	// Transport callers decrypt stored bucket_url so the response layer can
	// redact credential query parameters. Standalone AK/SK fields stay encrypted.
	auditLogSecretScopeTransport auditLogSecretScope = iota
	// Execution callers operate on cloned entities and need plaintext material
	// to build the regional replay request.
	auditLogSecretScopeExecution
)

// encryptReplayAuditLogSecretFields protects replay-level and task-level
// audit-log material before persistence.
func encryptReplayAuditLogSecretFields(encryptor encryptor_wrapper.Encryptor, replayEnt *entity.Replay) bizerr.BizError {
	return walkReplayAuditLogAddrs(replayEnt, func(addr *entity.AuditLogAddr) bizerr.BizError {
		return encryptAliyunAuditLogSecretFields(encryptor, addr.Aliyun)
	})
}

// encryptReplayTaskAuditLogSecretFields protects one task's audit-log material
// during task update merging using the new task payload as replacement input.
func encryptReplayTaskAuditLogSecretFields(
	encryptor encryptor_wrapper.Encryptor,
	task *entity.ReplayTask,
) bizerr.BizError {
	return walkReplayTaskAuditLogAddrs(task, func(addr *entity.AuditLogAddr) bizerr.BizError {
		return encryptAuditLogAddrSecretFields(encryptor, addr)
	})
}

// decryptReplayAuditLogSecretFields restores audit-log material according to the
// caller boundary. Execution callers must pass a clone rather than DB-owned
// entities because execution scope materializes standalone AK/SK.
func decryptReplayAuditLogSecretFields(
	encryptor encryptor_wrapper.Encryptor,
	replayEnt *entity.Replay,
	scope auditLogSecretScope,
) bizerr.BizError {
	return walkReplayAuditLogAddrs(replayEnt, func(addr *entity.AuditLogAddr) bizerr.BizError {
		return decryptAliyunAuditLogFields(encryptor, addr.Aliyun, scope)
	})
}

// walkReplayTaskAuditLogAddrs visits all audit-log inputs attached to one task.
func walkReplayTaskAuditLogAddrs(
	task *entity.ReplayTask,
	fn func(*entity.AuditLogAddr) bizerr.BizError,
) bizerr.BizError {
	if task == nil || fn == nil {
		return nil
	}
	for i := range task.DataSource.SourceAuditLogAddrs {
		if err := fn(&task.DataSource.SourceAuditLogAddrs[i]); err != nil {
			return err
		}
	}
	return nil
}

// encryptAuditLogAddrSecretFields is the provider dispatch point for
// persistence-time audit-log protection.
func encryptAuditLogAddrSecretFields(
	encryptor encryptor_wrapper.Encryptor,
	addr *entity.AuditLogAddr,
) bizerr.BizError {
	if addr == nil {
		return nil
	}
	return encryptAliyunAuditLogSecretFields(encryptor, addr.Aliyun)
}

// encryptAliyunAuditLogSecretFields validates the optional AK/SK pair and
// encrypts Aliyun audit-log fields before storage. BucketURL is encrypted as a
// full blob because it may contain credentials in query parameters.
// Examples:
//   - BucketURL "oss://bucket/path" becomes
//     "encrypted:aliyun-bucket-url:v1:<cipher>".
//   - Caller input with the reserved prefix is still encrypted as plaintext
//     input; the prefix is trusted only after DB load.
//   - AccessKeyID "ak" and SecretAccessKey "sk" are stored as ciphertext.
func encryptAliyunAuditLogSecretFields(encryptor encryptor_wrapper.Encryptor, aliyun *entity.AliyunAuditLog) bizerr.BizError {
	if aliyun == nil {
		return nil
	}
	hasAccessKeyID := strings.TrimSpace(aliyun.AccessKeyID) != ""
	hasSecretAccessKey := strings.TrimSpace(aliyun.SecretAccessKey) != ""
	if hasAccessKeyID != hasSecretAccessKey {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"access_key_id and secret_access_key must be provided together",
		)
	}
	if bizErr := encryptAliyunAuditLogBucketURL(encryptor, aliyun); bizErr != nil {
		return bizErr
	}
	if hasAccessKeyID {
		if err := encryptor.EncryptBase64Inplace(&aliyun.AccessKeyID); err != nil {
			return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown,
				"failed to encrypt Aliyun audit log access_key_id")
		}
		if err := encryptor.EncryptBase64Inplace(&aliyun.SecretAccessKey); err != nil {
			return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown,
				"failed to encrypt Aliyun audit log secret_access_key")
		}
	}
	return nil
}

func encryptAliyunAuditLogBucketURL(
	encryptor encryptor_wrapper.Encryptor,
	aliyun *entity.AliyunAuditLog,
) bizerr.BizError {
	if aliyun == nil || aliyun.BucketURL == "" {
		return nil
	}
	// bucket_url is caller-controlled at this boundary; even a value with the
	// reserved prefix must be encrypted as plaintext input.
	encryptedBucketURL, bizErr := encryptor.EncryptBase64(aliyun.BucketURL)
	if bizErr != nil {
		return bizErr.Chain("encrypt Aliyun audit log bucket_url")
	}
	aliyun.BucketURL = encryptedAliyunBucketURLPrefix + encryptedBucketURL
	return nil
}

// decryptAliyunAuditLogFields restores Aliyun audit-log fields according to the
// caller boundary.
// Example:
//   - Transport scope decrypts BucketURL only.
//   - Execution scope decrypts BucketURL, AccessKeyID, and SecretAccessKey.
func decryptAliyunAuditLogFields(
	encryptor encryptor_wrapper.Encryptor,
	aliyun *entity.AliyunAuditLog,
	scope auditLogSecretScope,
) bizerr.BizError {
	if aliyun == nil {
		return nil
	}
	if bizErr := decryptAliyunAuditLogBucketURL(encryptor, aliyun); bizErr != nil {
		return bizErr
	}
	if scope != auditLogSecretScopeExecution {
		return nil
	}
	if aliyun.AccessKeyID != "" {
		decryptedAccessKeyID, bizErr := encryptor.DecryptBase64(aliyun.AccessKeyID)
		if bizErr != nil {
			return bizErr.Chain("decrypt Aliyun audit log access_key_id")
		}
		aliyun.AccessKeyID = decryptedAccessKeyID
	}
	if aliyun.SecretAccessKey != "" {
		decryptedSecretAccessKey, bizErr := encryptor.DecryptBase64(aliyun.SecretAccessKey)
		if bizErr != nil {
			return bizErr.Chain("decrypt Aliyun audit log secret_access_key")
		}
		aliyun.SecretAccessKey = decryptedSecretAccessKey
	}
	return nil
}

// decryptAliyunAuditLogBucketURL trusts the storage prefix only after the value
// has been loaded from DB-managed state.
// Example:
//   - "encrypted:aliyun-bucket-url:v1:<cipher(oss://bucket/path)>" becomes
//     "oss://bucket/path".
func decryptAliyunAuditLogBucketURL(
	encryptor encryptor_wrapper.Encryptor,
	aliyun *entity.AliyunAuditLog,
) bizerr.BizError {
	if aliyun == nil || !strings.HasPrefix(aliyun.BucketURL, encryptedAliyunBucketURLPrefix) {
		return nil
	}
	encryptedBucketURL := strings.TrimPrefix(aliyun.BucketURL, encryptedAliyunBucketURLPrefix)
	bucketURL, bizErr := encryptor.DecryptBase64(encryptedBucketURL)
	if bizErr != nil {
		return bizErr.Chain("decrypt Aliyun audit log bucket_url")
	}
	aliyun.BucketURL = bucketURL
	return nil
}

// walkReplayAuditLogAddrs visits replay-level defaults first, then task-level
// overrides. The callback receives mutable pointers so callers can encrypt or
// decrypt in place on the entity they were given.
func walkReplayAuditLogAddrs(
	replayEnt *entity.Replay,
	fn func(*entity.AuditLogAddr) bizerr.BizError,
) bizerr.BizError {
	if replayEnt == nil || fn == nil {
		return nil
	}
	if replayEnt.SourceAuditCredential != nil {
		if err := fn(replayEnt.SourceAuditCredential); err != nil {
			return err
		}
	}
	for i := range replayEnt.ReplayTasks {
		if err := walkReplayTaskAuditLogAddrs(&replayEnt.ReplayTasks[i], fn); err != nil {
			return err
		}
	}
	return nil
}
