package service

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"log/slog"
	"sort"

	"github.com/tidbcloud/config-center/internal/db"
	pb "github.com/tidbcloud/config-center/proto/config_center"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/emptypb"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func (s *ConfigCenterService) CreateConfigurationTemplate(req *pb.CreateConfigurationTemplateRequest) (*pb.ConfigurationTemplate, error) {
	tx, err := s.DB.Begin()
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	template, err := CreateConfigurationTemplateInTx(tx, req)
	if err != nil {
		return nil, err
	}

	for region, cli := range s.GetRegionalClients(req.GetClusterType()) {
		// submit request to region
		_, err := cli.CreateConfigurationTemplate(context.Background(), &pb.CreateConfigurationTemplateRequest{
			TemplateId:        &template.TemplateId,
			ClusterType:       template.ClusterType,
			ConfigurationType: template.ConfigurationType,
			TemplateName:      template.TemplateName,
			Data:              template.Data,
			Tags:              template.Tags,
		})
		if err != nil {
			slog.Error("failed to finish calling regional CreateConfigurationTemplate", "region", region, "template_id", template.TemplateId, "error", err)
		} else {
			slog.Info("submited CreateConfigurationTemplate to region", "region", region, "template_id", template.TemplateId)
		}
	}

	return template, tx.Commit()
}

// calculateTemplateHash computes a deterministic SHA256 hash from template data only.
// Hash includes only the configuration data (key-value pairs with comments).
// This ensures that adding new metadata fields to the template won't invalidate the hash.
func calculateTemplateHash(data map[string]*pb.ParamItem) string {
	h := sha256.New()

	// Write data in sorted order for determinism
	var dataKeys []string
	for k := range data {
		dataKeys = append(dataKeys, k)
	}
	sort.Strings(dataKeys)
	for _, k := range dataKeys {
		h.Write([]byte(k))
		h.Write([]byte{0})
		if data[k] != nil && data[k].Value != nil {
			valueBytes, _ := json.Marshal(data[k].Value.AsInterface())
			h.Write(valueBytes)
		}
		h.Write([]byte{0})
		if data[k] != nil {
			h.Write([]byte(data[k].Comment))
		}
		h.Write([]byte{0})
	}

	return hex.EncodeToString(h.Sum(nil))
}

// tagsEqual checks if two tag maps are equal
func tagsEqual(a, b map[string]string) bool {
	if len(a) != len(b) {
		return false
	}
	for k, v := range a {
		if b[k] != v {
			return false
		}
	}
	return true
}

func CreateConfigurationTemplateInTx(tx db.DB, req *pb.CreateConfigurationTemplateRequest) (*pb.ConfigurationTemplate, error) {
	tagBytes, err := json.Marshal(req.GetTags())
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "tags invalid")
	}

	// Calculate content hash for deduplication.
	// Hash only includes data (key-value pairs), not metadata fields.
	// This ensures adding new fields won't invalidate existing hashes.
	contentHash := calculateTemplateHash(req.GetData())

	// Content-hash dedup is only applied when the caller lets us assign the ID.
	// When an explicit template_id is provided (e.g. the global->regional sync,
	// which carries the global IDs so default-template bindings resolve on the
	// region), we must honor that ID instead of collapsing it onto an existing
	// row with the same content. Otherwise the region stores the content under a
	// different ID and any binding referencing the requested ID 404s.
	if req.TemplateId == nil {
		// Check if a template with the same hash already exists
		// Query by cluster_type + config_type + content_hash, then filter by name + tags in code
		candidates, err := tx.ListTemplatesByHash(req.GetClusterType().String(), req.GetConfigurationType(), contentHash)
		if err != nil {
			return nil, err
		}

		// Find matching template by name and tags
		for _, candidate := range candidates {
			var candidateTags map[string]string
			if candidate.Tags != nil {
				if err := json.Unmarshal([]byte(*candidate.Tags), &candidateTags); err != nil {
					continue
				}
			}
			if candidate.TemplateName == req.GetTemplateName() && tagsEqual(candidateTags, req.GetTags()) {
				// Found existing template with same content, return it
				slog.Info("reusing existing template with same content", "template_id", candidate.ID, "content_hash", contentHash)
				templateValues, err := tx.ListTemplateValueRows(candidate.ID)
				if err != nil {
					return nil, err
				}
				resp, err := buildTemplate(candidate, templateValues)
				if err != nil {
					return nil, err
				}
				return &resp, nil
			}
		}
	}

	id, err := tx.CreateTemplateRow(req.TemplateId, req.GetClusterType().String(), req.GetConfigurationType(), req.GetTemplateName(), string(tagBytes), contentHash)
	if err == db.ErrAlreadyExists {
		// A row with the requested ID already exists. For the explicit-ID path this
		// is the expected outcome of a re-run sync, so treat it as idempotent and
		// return the existing template instead of failing the whole sync.
		if req.TemplateId != nil {
			existing, err := tx.GetTemplateRow(req.GetTemplateId())
			if err != nil {
				return nil, err
			}
			templateValues, err := tx.ListTemplateValueRows(existing.ID)
			if err != nil {
				return nil, err
			}
			resp, err := buildTemplate(existing, templateValues)
			if err != nil {
				return nil, err
			}
			resp.ClusterType = req.GetClusterType()
			return &resp, nil
		}
		return nil, status.Errorf(codes.AlreadyExists, "template %d already exists", req.GetTemplateId())
	} else if err != nil {
		return nil, err
	}

	var values []db.TemplateValueRow
	for k, v := range req.GetData() {
		values = append(values, db.TemplateValueRow{
			TemplateID: id,
			Key:        k,
			Value:      db.NewAnyValue(v.Value.AsInterface()),
			Comment:    v.Comment,
		})
	}

	if len(values) > 0 {
		err = tx.InsertTemplateValueRows(values)
		if err != nil {
			return nil, err
		}
	}

	return &pb.ConfigurationTemplate{
		ClusterType:       req.GetClusterType(),
		TemplateId:        id,
		TemplateName:      req.GetTemplateName(),
		ConfigurationType: req.GetConfigurationType(),
		Data:              req.GetData(),
		Tags:              req.GetTags(),
	}, nil
}

func GetConfigurationTemplate(d db.DB, req *pb.GetConfigurationTemplateRequest) (*pb.ConfigurationTemplate, error) {
	tx, err := d.Begin()
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	if req.GetTemplateId() == 0 {
		return &pb.ConfigurationTemplate{
			ClusterType:  req.GetClusterType(),
			TemplateId:   0,
			TemplateName: "empty",
			Data:         make(map[string]*pb.ParamItem),
		}, tx.Commit()
	}

	// template may be used in other cluster type
	template, err := tx.GetTemplateRow(req.GetTemplateId())
	if err == db.ErrNotFound {
		return nil, status.Errorf(codes.NotFound, "template %d not found", req.GetTemplateId())
	} else if err != nil {
		return nil, err
	}

	templateValues, err := tx.ListTemplateValueRows(req.GetTemplateId())
	if err != nil {
		return nil, err
	}

	resp, err := buildTemplate(template, templateValues)
	if err != nil {
		return nil, err
	}

	// Use cluster type from request
	resp.ClusterType = req.GetClusterType()

	return &resp, tx.Commit()
}

func ListConfigurationTemplates(d db.DB, req *pb.ListConfigurationTemplatesRequest) (*pb.ListConfigurationTemplatesResponse, error) {
	tx, err := d.Begin()
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	templates, err := tx.ListTemplateRows(req.GetClusterType().String(), req.GetConfigType(), req.GetTags(), "", 0, 0)
	if err != nil {
		return nil, err
	}

	resp := &pb.ListConfigurationTemplatesResponse{}
	for _, tow := range templates {
		template, err := GetConfigurationTemplate(tx, &pb.GetConfigurationTemplateRequest{
			ClusterType:       req.ClusterType,
			ConfigurationType: req.GetConfigType(),
			TemplateId:        tow.ID,
		})
		if err != nil {
			return nil, err
		}
		resp.Templates = append(resp.Templates, template)
	}

	return resp, tx.Commit()
}

func DeleteConfigurationTemplate(d db.DB, req *pb.DeleteConfigurationTemplateRequest) (*emptypb.Empty, error) {
	tx, err := d.Begin()
	if err != nil {
		return nil, err
	}

	templateID := req.GetTemplateId()

	num, err := tx.CountClusterConfigInfoByTemplateID(templateID)
	if err != nil {
		tx.Rollback()
		return nil, err
	}
	if num > 0 {
		tx.Rollback()
		return nil, status.Errorf(codes.FailedPrecondition, "template is used by exist cluster(s)")
	}

	num, err = tx.CountDefaultTemplateByTemplateID(templateID)
	if err != nil {
		tx.Rollback()
		return nil, err
	}
	if num > 0 {
		tx.Rollback()
		return nil, status.Errorf(codes.FailedPrecondition, "template is a default template")
	}

	// Soft delete template only, keep rows in template_values
	// err = tx.DeleteTemplateValueRows(templateID)
	// if err != nil {
	// 	tx.Rollback()
	// 	return nil, err
	// }

	err = tx.DeleteTemplateRow(templateID)
	if err != nil {
		tx.Rollback()
		return nil, err
	}

	return &emptypb.Empty{}, tx.Commit()
}

func SearchDefaultConfigurationTemplate(d db.DB, req *pb.SearchDefaultConfigurationTemplateRequest) (*pb.ConfigurationTemplate, error) {
	tx, err := d.Begin()
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	// FIXME: not hardcode
	UnifiedService := ConfigCenterService{DB: d, ServerRegion: ServerRegion}
	templateID, err := UnifiedService.SearchDefaultConfigurationTemplateIdInTx(tx, req)
	switch err {
	case nil:
		// found
	case db.ErrNotFound:
		return nil, status.Errorf(codes.NotFound, "[%s] default template for config type %s on version %s and instance %s.%s not found", req.GetClusterType().String(), req.GetConfigurationType(), req.GetVersion(), req.GetCloudProvider(), req.GetNodeInstance())
	default:
		return nil, err
	}

	t, err := GetConfigurationTemplate(tx, &pb.GetConfigurationTemplateRequest{
		ClusterType:       req.GetClusterType(),
		ConfigurationType: req.GetConfigurationType(),
		TemplateId:        templateID,
	})
	if err != nil {
		return nil, err
	}

	return t, tx.Commit()
}

// searchDefaultConfigurationTemplateIdDirectly tries to find the default template ID by (version, cloud_provider, node_instance, tenant_id)
//
// Matching priority:
// * (version, cloud_provider, node_instance, tenant_id)
// * (version, cloud_provider, node_instance, any)
// * (version, any, any, any)
// * (any, any, any, any)
func searchDefaultConfigurationTemplateIdDirectly(tx db.DB, req *pb.SearchDefaultConfigurationTemplateRequest) (int64, error) {
	region := req.GetRegion()
	templateID, err := tx.GetDefaultTemplate(req.GetClusterType().String(), req.GetConfigurationType(), req.GetVersion(), req.GetCloudProvider(), req.GetNodeInstance(), region, req.GetTenantId())
	if err == nil {
		return templateID, nil
	} else if err != db.ErrNotFound {
		return 0, err
	}

	templateID, err = tx.GetDefaultTemplate(req.GetClusterType().String(), req.GetConfigurationType(), req.GetVersion(), req.GetCloudProvider(), req.GetNodeInstance(), region, "")
	if err == nil {
		return templateID, nil
	} else if err != db.ErrNotFound {
		return 0, err
	}

	// for version、cloud_provider and node_instance nil means "any"
	templateID, err = tx.GetDefaultTemplate(req.GetClusterType().String(), req.GetConfigurationType(), req.GetVersion(), "" /*any cloud*/, "" /*any sku*/, region, "")
	if err == nil {
		return templateID, nil
	} else if err != db.ErrNotFound {
		return 0, err
	}

	templateID, err = tx.GetDefaultTemplate(req.GetClusterType().String(), req.GetConfigurationType(), "" /*any version*/, "" /*any cloud*/, "" /*any sku*/, region, "")
	if err == nil {
		return templateID, nil
	} else if err != db.ErrNotFound {
		return 0, err
	}

	return 0, db.ErrNotFound
}

func (s *ConfigCenterService) SearchDefaultConfigurationTemplateIdInTx(tx db.DB, req *pb.SearchDefaultConfigurationTemplateRequest) (int64, error) {
	region := req.GetRegion()
	// in regional mode, server region is the default region if not specified
	if s.ServerRegion != "" {
		region = s.ServerRegion
	}
	// only support INITSQL for regional grayscale (BYOC)
	if region != "" && req.GetClusterType() == pb.ClusterType_SHARED_POOL && req.GetConfigurationType() == pb.SHARED_POOL_CONFIG_INITSQL {
		reqWithRegion := proto.Clone(req).(*pb.SearchDefaultConfigurationTemplateRequest)
		reqWithRegion.Region = &region
		templateID, err := searchDefaultConfigurationTemplateIdDirectly(tx, reqWithRegion)
		if err == nil {
			return templateID, nil
		} else if err != db.ErrNotFound {
			return 0, err
		}
	}

	reqWithoutRegion := proto.Clone(req).(*pb.SearchDefaultConfigurationTemplateRequest)
	reqWithoutRegion.Region = nil
	templateID, err := searchDefaultConfigurationTemplateIdDirectly(tx, reqWithoutRegion)
	if err == nil {
		return templateID, nil
	} else if err != db.ErrNotFound {
		return 0, err
	}

	return 0, db.ErrNotFound
}

func (s *ConfigCenterService) UpdateDefaultConfigurationTemplateInTx(tx db.DB, req *pb.UpdateDefaultConfigurationTemplateRequest) (*pb.ConfigurationTemplate, error) {
	regions := req.GetRegions()
	if len(regions) == 0 {
		regions = []string{""} // "" means any region
	}

	for _, region := range regions {
		_, err := tx.GetDefaultTemplate(req.GetClusterType().String(), req.GetConfigurationType(), req.GetVersion(), req.GetCloudProvider(), req.GetNodeInstance(), region, req.GetTenantId())
		switch err {
		case nil:
			err = tx.UpdateDefaultTemplate(req.GetClusterType().String(), req.GetConfigurationType(), req.GetVersion(), req.GetCloudProvider(), req.GetNodeInstance(), region, req.GetTenantId(), req.GetTemplateId())
		case db.ErrNotFound:
			err = tx.InsertDefaultTemplate(req.GetClusterType().String(), req.GetConfigurationType(), req.GetVersion(), req.GetCloudProvider(), req.GetNodeInstance(), region, req.GetTenantId(), req.GetTemplateId())
		}
		if err != nil {
			return nil, err
		}
	}

	template, err := GetConfigurationTemplate(tx, &pb.GetConfigurationTemplateRequest{
		ClusterType:       req.GetClusterType(),
		ConfigurationType: req.GetConfigurationType(),
		TemplateId:        req.GetTemplateId(),
	})
	if err != nil {
		return nil, err
	}

	for region, cli := range s.GetRegionalClients(req.GetClusterType()) {
		_, err := cli.UpdateDefaultConfigurationTemplate(context.Background(), req)
		if err != nil {
			slog.Error("failed to finish calling regional UpdateDefaultConfigurationTemplate", "region", region, "error", err)
		}
	}

	return template, nil
}

func (s *ConfigCenterService) DeleteDefaultConfigurationTemplateInTx(tx db.DB, req *pb.DeleteDefaultConfigurationTemplateRequest) (*emptypb.Empty, error) {
	regions := req.GetRegions()
	if len(regions) == 0 {
		regions = []string{""} // "" means any region
	}

	for _, region := range regions {
		_, err := tx.GetDefaultTemplate(req.GetClusterType().String(), req.GetConfigurationType(), req.GetVersion(), req.GetCloudProvider(), req.GetNodeInstance(), region, req.GetTenantId())
		if err != nil {
			return nil, err
		}

		err = tx.DeleteDefaultTemplate(req.GetClusterType().String(), req.GetConfigurationType(), req.GetVersion(), req.GetCloudProvider(), req.GetNodeInstance(), region, req.GetTenantId())
		if err != nil {
			return nil, err
		}
	}

	for region, cli := range s.GetRegionalClients(req.GetClusterType()) {
		_, err := cli.DeleteDefaultConfigurationTemplate(context.Background(), req)
		if err != nil {
			slog.Error("failed to finish calling regional DeleteDefaultConfigurationTemplate", "region", region, "error", err)
		}
	}

	return &emptypb.Empty{}, nil
}

func buildTemplate(info db.TemplateRow, values []db.TemplateValueRow) (pb.ConfigurationTemplate, error) {
	template := pb.ConfigurationTemplate{
		ClusterType:       pb.ClusterType(pb.ClusterType_value[info.ClusterType]),
		TemplateId:        info.ID,
		TemplateName:      info.TemplateName,
		ConfigurationType: info.ConfigType,
		Data:              make(map[string]*pb.ParamItem, len(values)),
		CreateTime:        timestamppb.New(info.CreateTime),
		UpdateTime:        timestamppb.New(info.UpdateTime),
	}
	if info.DeleteTime != nil {
		template.DeleteTime = timestamppb.New(*info.DeleteTime)
	}

	if info.Tags != nil {
		var tags map[string]string
		err := json.Unmarshal([]byte(*info.Tags), &tags)
		if err != nil {
			return template, err
		}
		template.Tags = tags
	}

	for _, row := range values {
		value, err := pb.NewValue(row.Value.Raw)
		if err != nil {
			return template, err
		}
		template.Data[row.Key] = &pb.ParamItem{
			Value:   value,
			Comment: row.Comment,
		}
	}

	return template, nil
}
