package pg_backup_proxy

import (
	"context"
	"hash/fnv"
	"strings"
	"time"

	"go.opentelemetry.io/otel"
	"google.golang.org/protobuf/types/known/timestamppb"

	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/adapter"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/repository"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

var Tracer = otel.Tracer("app.pg_backup_proxy")

const (
	backupLabelSourceBackupID = "sys9_backup_id"
	backupLabelSourceState    = "sys9_backup_state"
	backupLabelSourceExpireAt = "sys9_backup_expire_at"
	defaultBackupSchedule     = "BACKUP_SCHEDULE_DAILY"
	maxLookupPages            = 100
	maxLookupPageSize         = 100
)

type service struct {
	globalserverlessv1.UnimplementedServerlessPGBackupProxyServiceServer

	clusterRepo   repository.ClusterRepo
	sys9Lifecycle adapter.Sys9Lifecycle
}

func NewService(
	clusterRepo repository.ClusterRepo,
	sys9Lifecycle adapter.Sys9Lifecycle,
) globalserverlessv1.ServerlessPGBackupProxyServiceServer {
	return &service{clusterRepo: clusterRepo, sys9Lifecycle: sys9Lifecycle}
}

func (s *service) CreatePGBackup(
	ctx context.Context,
	req *globalserverlessv1.CreatePGBackupRequest,
) (*globalserverlessv1.CreatePGBackupResponse, error) {
	ctx, span := Tracer.Start(ctx, "CreatePGBackup")
	defer span.End()

	cluster, err := s.loadPGCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}

	operationID, err := s.sys9Lifecycle.CreateManagedDatabaseBackup(ctx, cluster, cluster.ExternalInstanceID)
	if err != nil {
		return nil, err
	}
	operation, err := s.sys9Lifecycle.GetOperation(ctx, cluster, operationID)
	if err != nil {
		return nil, err
	}
	if operation == nil || strings.TrimSpace(operation.ResourceID) == "" {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "sys9 create managed database backup missing backup id")
	}

	return &globalserverlessv1.CreatePGBackupResponse{BackupId: hashBackupID(operation.ResourceID)}, nil
}

func (s *service) ListPGBackups(
	ctx context.Context,
	req *globalserverlessv1.ListPGBackupsRequest,
) (*globalserverlessv1.ListPGBackupsResponse, error) {
	ctx, span := Tracer.Start(ctx, "ListPGBackups")
	defer span.End()

	cluster, err := s.loadPGCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}

	out, err := s.sys9Lifecycle.ListManagedDatabaseBackups(ctx, cluster, cluster.ExternalInstanceID, &adapter.Sys9ListManagedDatabaseBackupsRequest{
		LastBackupTime: timestampPtr(req.GetLastBackupTime()),
		MaxCount:       req.GetMaxCount(),
	})
	if err != nil {
		return nil, err
	}

	items := make([]*globalserverlessv1.PGPhysicalBackup, 0, len(out.Items))
	for _, item := range out.Items {
		items = append(items, toPhysicalBackup(item))
	}

	return &globalserverlessv1.ListPGBackupsResponse{Items: items, HasMore: out.HasMore}, nil
}

func (s *service) GetPGBackup(
	ctx context.Context,
	req *globalserverlessv1.GetPGBackupRequest,
) (*globalserverlessv1.GetPGBackupResponse, error) {
	ctx, span := Tracer.Start(ctx, "GetPGBackup")
	defer span.End()

	if req.GetBackupId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "backup id is required")
	}

	cluster, err := s.loadPGCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}

	sourceBackupID, err := s.resolveSourceBackupID(ctx, cluster, req.GetBackupId())
	if err != nil {
		return nil, err
	}

	out, err := s.sys9Lifecycle.GetManagedDatabaseBackup(ctx, cluster, cluster.ExternalInstanceID, sourceBackupID)
	if err != nil {
		return nil, err
	}

	return &globalserverlessv1.GetPGBackupResponse{Backup: toPhysicalBackup(out)}, nil
}

func (s *service) GetPGBackupPolicy(
	ctx context.Context,
	req *globalserverlessv1.GetPGBackupPolicyRequest,
) (*globalserverlessv1.GetPGBackupPolicyResponse, error) {
	ctx, span := Tracer.Start(ctx, "GetPGBackupPolicy")
	defer span.End()

	cluster, err := s.loadPGCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}

	policy, err := s.sys9Lifecycle.GetManagedDatabaseBackupPolicy(ctx, cluster, cluster.ExternalInstanceID)
	if err != nil {
		return nil, err
	}

	return &globalserverlessv1.GetPGBackupPolicyResponse{BackupPolicy: toGlobalBackupPolicy(policy)}, nil
}

func (s *service) UpdatePGBackupPolicy(
	ctx context.Context,
	req *globalserverlessv1.UpdatePGBackupPolicyRequest,
) (*globalserverlessv1.UpdatePGBackupPolicyResponse, error) {
	ctx, span := Tracer.Start(ctx, "UpdatePGBackupPolicy")
	defer span.End()

	if req.GetBackupPolicy() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "backup policy is required")
	}
	if req.GetBackupPolicy().GetRetentionDays() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "backup policy retention days must be greater than 0")
	}

	cluster, err := s.loadPGCluster(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}

	schedule := strings.TrimSpace(req.GetBackupPolicy().GetBackupTime())
	if schedule == "" {
		schedule = defaultBackupSchedule
	}

	out, err := s.sys9Lifecycle.UpdateManagedDatabaseBackupPolicy(ctx, cluster, cluster.ExternalInstanceID, &adapter.Sys9ManagedDatabaseBackupPolicy{
		Enabled:       true,
		Schedule:      schedule,
		RetentionDays: uint32(req.GetBackupPolicy().GetRetentionDays()),
	})
	if err != nil {
		return nil, err
	}

	return &globalserverlessv1.UpdatePGBackupPolicyResponse{BackupPolicy: toGlobalBackupPolicy(out)}, nil
}

func (s *service) loadPGCluster(ctx context.Context, clusterID string) (*entity.Cluster, error) {
	if strings.TrimSpace(clusterID) == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster id is required")
	}
	cluster, err := s.clusterRepo.Get(ctx, clusterID, false)
	if err != nil {
		return nil, err
	}
	if cluster == nil {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "cluster %s not found", clusterID)
	}
	if !strings.EqualFold(cluster.EngineType, "POSTGRESQL_COMPATIBLE") {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster %s is not postgres compatible", clusterID)
	}
	if cluster.Region == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster %s missing region", clusterID)
	}
	if strings.TrimSpace(cluster.ExternalInstanceID) == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster %s missing external instance id", clusterID)
	}
	return cluster, nil
}

func (s *service) resolveSourceBackupID(ctx context.Context, cluster *entity.Cluster, backupID uint64) (string, error) {
	pageToken := ""
	for range maxLookupPages {
		resp, err := s.sys9Lifecycle.ListManagedDatabaseBackups(
			ctx,
			cluster,
			cluster.ExternalInstanceID,
			&adapter.Sys9ListManagedDatabaseBackupsRequest{
				MaxCount:  maxLookupPageSize,
				PageToken: pageToken,
			},
		)
		if err != nil {
			return "", err
		}
		for _, item := range resp.Items {
			if item != nil && hashBackupID(item.BackupID) == backupID {
				return item.BackupID, nil
			}
		}
		if !resp.HasMore || strings.TrimSpace(resp.NextPageToken) == "" {
			break
		}
		pageToken = resp.NextPageToken
	}
	return "", bizerr.New(bizerr.ErrCodeRecordNotFound, "backup %d not found", backupID)
}

func toPhysicalBackup(item *adapter.Sys9ManagedDatabaseBackup) *globalserverlessv1.PGPhysicalBackup {
	if item == nil {
		return &globalserverlessv1.PGPhysicalBackup{}
	}
	labels := map[string]string{
		backupLabelSourceBackupID: item.BackupID,
		backupLabelSourceState:    item.State,
	}
	if item.Expiration != nil {
		labels[backupLabelSourceExpireAt] = item.Expiration.UTC().Format(time.RFC3339)
	}

	return &globalserverlessv1.PGPhysicalBackup{
		BackupId:   hashBackupID(item.BackupID),
		BackupName: item.BackupName,
		BackupTime: timestamppb.New(item.BackupTime),
		Labels:     labels,
	}
}

func toGlobalBackupPolicy(policy *adapter.Sys9ManagedDatabaseBackupPolicy) *globalserverlessv1.AutomatedBackupPolicy {
	if policy == nil {
		return &globalserverlessv1.AutomatedBackupPolicy{}
	}
	return &globalserverlessv1.AutomatedBackupPolicy{
		BackupTime:    strings.TrimSpace(policy.Schedule),
		RetentionDays: uint64(policy.RetentionDays),
	}
}

func hashBackupID(id string) uint64 {
	h := fnv.New64a()
	_, _ = h.Write([]byte(id))
	return h.Sum64()
}

func timestampPtr(ts *timestamppb.Timestamp) *time.Time {
	if ts == nil {
		return nil
	}
	t := ts.AsTime().UTC()
	return &t
}
