package auditlog

import (
	"context"

	"golang.org/x/mod/semver"
	"google.golang.org/protobuf/types/known/emptypb"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	auditlogsvc "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/service/auditlog"
	auditlogsvclegacy "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/service/auditlog/legacyimpl"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/service/cluster"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/featuregate"
)

func NewServer(
	legacySvc auditlogsvclegacy.Service,
	v2Svc auditlogsvc.Service,
	clusterSvc cluster.Service,
	featGate featuregate.Service,
) globaldedicatedv1.DatabaseAuditLogServiceServer {
	return &server{
		legacySvc:  legacySvc,
		v2Svc:      v2Svc,
		clusterSvc: clusterSvc,
		featGate:   featGate,
	}
}

type server struct {
	legacySvc  auditlogsvclegacy.Service
	v2Svc      auditlogsvc.Service
	clusterSvc cluster.Service
	featGate   featuregate.Service
}

func (s *server) GetAccount(
	ctx context.Context,
	req *globaldedicatedv1.GetAccountRequest,
) (*globaldedicatedv1.GetAccountResponse, error) {
	account, err := s.v2Svc.GetAccount(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	return &globaldedicatedv1.GetAccountResponse{
		Account: account,
	}, nil
}

func (s *server) GetAuditLogConfig(
	ctx context.Context,
	req *globaldedicatedv1.GetAuditLogConfigRequest,
) (*globaldedicatedv1.AuditLogConfig, error) {
	isV2, err := s.isV2(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	var config *globaldedicatedv1.AuditLogConfig
	if isV2 {
		config, err = s.v2Svc.GetAuditLogConfig(ctx, req.GetClusterId())
		if err != nil {
			return nil, err
		}
	} else {
		config, err = s.legacySvc.GetAuditLogConfig(ctx, req.GetClusterId())
		if err != nil {
			return nil, err
		}
	}
	return config, nil
}

func (s *server) UpdateAuditLogConfig(
	ctx context.Context,
	req *globaldedicatedv1.UpdateAuditLogConfigRequest,
) (*globaldedicatedv1.AuditLogConfig, error) {
	isV2, err := s.isV2(ctx, req.GetAuditLogConfig().GetClusterId())
	if err != nil {
		return nil, err
	}

	if isV2 {
		config, err := s.v2Svc.UpdateAuditLogConfig(ctx, req.GetAuditLogConfig(), req.GetValidateOnly())
		if err != nil {
			return nil, err
		}
		return config, nil
	}

	// Legacy audit log config.
	if req.GetValidateOnly() {
		result, err := s.legacySvc.PrecheckAuditLogConfig(ctx, req.GetAuditLogConfig())
		if err != nil {
			return nil, err
		}
		if !result.GetValid() {
			return nil, bizerr.New(bizerr.ErrCodePermission, "%s", result.GetMessage())
		}
		return req.GetAuditLogConfig(), nil
	}

	config, err := s.legacySvc.UpdateAuditLogConfig(ctx, req.GetAuditLogConfig())
	if err != nil {
		return nil, err
	}

	return config, nil
}

func (s *server) PrecheckAuditLogConfig(
	ctx context.Context,
	req *globaldedicatedv1.PrecheckAuditLogConfigRequest,
) (*globaldedicatedv1.PrecheckAuditLogConfigResponse, error) {
	isV2, err := s.isV2(ctx, req.GetAuditLogConfig().GetClusterId())
	if err != nil {
		return nil, err
	}

	if isV2 {
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"PrecheckAuditLogConfig is not supported for v2 audit log config, use UpdateAuditLogConfig with validateOnly=true instead",
		)
	}

	result, err := s.legacySvc.PrecheckAuditLogConfig(ctx, req.GetAuditLogConfig())
	if err != nil {
		return nil, err
	}

	return result, nil
}

func (s *server) ListAuditLogFilterRules(
	ctx context.Context,
	req *globaldedicatedv1.ListAuditLogFilterRulesRequest,
) (*globaldedicatedv1.ListAuditLogFilterRulesResponse, error) {
	isV2, err := s.isV2(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	var filterRules []*globaldedicatedv1.AuditLogFilterRule
	if isV2 {
		filterRules, err = s.v2Svc.ListAuditLogFilterRules(ctx, req.GetClusterId())
		if err != nil {
			return nil, err
		}
	} else {
		filterRules, err = s.legacySvc.ListAuditLogFilterRules(ctx, req.GetClusterId())
		if err != nil {
			return nil, err
		}
	}
	return &globaldedicatedv1.ListAuditLogFilterRulesResponse{
		Rules: filterRules,
	}, nil
}

func (s *server) CreateAuditLogFilterRule(
	ctx context.Context,
	req *globaldedicatedv1.CreateAuditLogFilterRuleRequest,
) (*globaldedicatedv1.AuditLogFilterRule, error) {
	isV2, err := s.isV2(ctx, req.GetRule().GetClusterId())
	if err != nil {
		return nil, err
	}
	var rule *globaldedicatedv1.AuditLogFilterRule
	if isV2 {
		rule, err = s.v2Svc.CreateAuditLogFilterRule(ctx, req.GetRule())
		if err != nil {
			return nil, err
		}
	} else {
		rule, err = s.legacySvc.CreateAuditLogFilterRule(ctx, req.GetRule().GetClusterId(), req.GetRule())
		if err != nil {
			return nil, err
		}
	}
	return rule, nil
}

func (s *server) DeleteAuditLogFilterRule(
	ctx context.Context,
	req *globaldedicatedv1.DeleteAuditLogFilterRuleRequest,
) (*emptypb.Empty, error) {
	isV2, err := s.isV2(ctx, req.GetClusterId())
	if err != nil {
		return nil, err
	}
	if isV2 {
		if err := s.v2Svc.DeleteAuditLogFilterRule(ctx, req); err != nil {
			return nil, err
		}
	} else {
		if err := s.legacySvc.DeleteAuditLogFilterRule(ctx, req); err != nil {
			return nil, err
		}
	}
	return &emptypb.Empty{}, nil
}

func (s *server) ReplaceAuditLogFilterRule(
	ctx context.Context,
	req *globaldedicatedv1.ReplaceAuditLogFilterRuleRequest,
) (*globaldedicatedv1.AuditLogFilterRule, error) {
	isV2, err := s.isV2(ctx, req.GetRule().GetClusterId())
	if err != nil {
		return nil, err
	}
	if !isV2 {
		return nil, bizerr.New(
			bizerr.ErrCodeUnknown,
			"ReplaceAuditLogFilterRule is not implemented for legacy audit log",
		)
	}
	rule, err := s.v2Svc.ReplaceAuditLogFilterRule(ctx, req.GetRule())
	if err != nil {
		return nil, err
	}
	return rule, nil
}

func (s *server) QueryAuditLogFiles(
	ctx context.Context,
	req *globaldedicatedv1.QueryAuditLogFilesRequest,
) (*globaldedicatedv1.QueryAuditLogFilesResponse, error) {
	auditlogs, err := s.v2Svc.QueryAuditLogFiles(ctx, req)
	if err != nil {
		return nil, err
	}
	return auditlogs, nil
}

func (s *server) GenerateAuditLogFileDownloadAddress(
	ctx context.Context,
	req *globaldedicatedv1.GenerateAuditLogFileDownloadAddressRequest,
) (*globaldedicatedv1.GenerateAuditLogFileDownloadAddressResponse, error) {
	url, err := s.v2Svc.GenerateAuditLogFileDownloadAddress(ctx, req)
	if err != nil {
		return nil, err
	}
	return url, nil
}

// A cluster should use audit log v2.0 if:
//   - In the v2.0 whitelist, and
//   - TiDB version >= v7.1.0, and
//   - Not (The cluster enable legacy audit log and has effected old rules).
func (s *server) isV2(ctx context.Context, clusterId uint64) (bool, bizerr.BizError) {
	// Get the cluster from repo first.
	clusters, err := s.clusterSvc.List(ctx, &globaldedicatedv1.ListClustersRequest{
		ClusterIds:  []uint64{clusterId},
		ShowDetails: false,
	})
	if err != nil {
		return false, bizerr.New(bizerr.ErrCodeUnknown, "failed to get cluster: %v", err)
	}
	if len(clusters) != 1 {
		return false, bizerr.New(bizerr.ErrCodeParams, "number of clusters is not 1: %d", len(clusters))
	}

	// Check if the feature gate is enabled.
	if !s.featGate.CheckFeatureEnabled(
		featuregate.AuditLogV2,
		clusters[0].GetOrganizationId(),
		clusters[0].GetProjectId(),
		clusters[0].GetCloudProviderId().String(),
		clusters[0].GetRegionId().String(),
	) {
		return false, nil
	}

	// Check if the cluster version is >= v7.1.0.
	ver := clusters[0].GetVersion()
	if !semver.IsValid(ver) {
		return false, bizerr.New(bizerr.ErrCodeParams, "invalid semver: %s", ver)
	}
	if semver.Compare(semver.Canonical(ver), auditlogsvc.V2MinVersion) < 0 {
		return false, nil
	}

	// Check if the legacy audit log is disabled or has effective filter rules.
	legacyConfig, err := s.legacySvc.GetAuditLogConfig(ctx, clusterId)
	if err != nil {
		return false, err
	}
	if legacyConfig.GetEnabled() {
		legacyRules, err := s.legacySvc.ListAuditLogFilterRules(ctx, clusterId)
		if err != nil {
			return false, err
		}
		if len(legacyRules) > 0 {
			return false, nil
		}
	}

	return true, nil
}
