package monitoring

import (
	"context"
	"fmt"

	"strconv"
	"strings"
	"unicode"

	"github.com/go-logr/logr"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"go.uber.org/dig"

	monent "github.com/tidbcloud/devops/entity/monitoring"
	"github.com/tidbcloud/devops/pkg/infra/asset"
	"github.com/tidbcloud/devops/pkg/infra/clients/cliniccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/jiracli"
	"github.com/tidbcloud/devops/pkg/infra/clients/larkcli"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/eventbus"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/service"
	msgapi "github.com/tidbcloud/devops/pkg/service/message/api"
	"github.com/tidbcloud/devops/pkg/service/monitoring/checker"
	"github.com/tidbcloud/devops/pkg/service/monitoring/checker/clusteranomalydetection"
	"github.com/tidbcloud/devops/pkg/service/monitoring/checker/idlereminder"
	"github.com/tidbcloud/devops/pkg/service/monitoring/repo"
	"github.com/tidbcloud/devops/pkg/service/tenant"
)

// nolint:gosec
const (
	DBPdClusterSecret        = "db-pd-cluster-secret"
	DBTikvClusterSecret      = "db-tikv-cluster-secret"
	DBTidbClusterSecret      = "db-tidb-cluster-secret"
	DBTiflashClusterSecret   = "db-tiflash-cluster-secret"
	DBTidbServerSecret       = "db-tidb-server-secret"
	DBClusterClientSecret    = "db-cluster-client-secret"
	ManualDBTidbClientSecret = "manual-db-tidb-client-secret"
	ProxyCertificate         = "proxy-certificate"
	DBClusterCa              = "db-cluster-ca"
	DBCdcClusterClientSecret = "db-cdc-cluster-client-secret"
	DBCdcPdClusterSecret     = "db-cdc-pd-cluster-secret"
)

type ServiceParam struct {
	dig.In

	Conf          *config.RootConfig
	Logger        logr.Logger
	Asset         asset.Asset
	EventBus      *eventbus.EventBus
	CoreSvcCli    *coresvccli.CoreSvcCli
	JiraClient    jiracli.JiraClient
	TenantService tenant.TenantServiceAPI
	MsgService    msgapi.MessageServiceAPI
	LarkCli       *larkcli.LarkClient
	ClinicCli     cliniccli.Client
}

type ServiceResult struct {
	dig.Out

	API     MonitoringServiceAPI
	Service service.Service `group:"service"`
}

type svc struct {
	*service.JobService

	conf config.MonitoringServiceConfig

	coreCli       *coresvccli.CoreSvcCli
	tenantService tenant.TenantServiceAPI
	msgService    msgapi.MessageServiceAPI
	larkCli       *larkcli.LarkClient
	eventbus      *eventbus.EventBus

	certChecker             *checker.CertChecker
	compDiffChecker         *checker.CompDiffChecker
	weeklyOncallChecker     *idlereminder.WeeklyOncallChecker
	jiraSpecCheckers        []*idlereminder.JiraSpecChecker
	clusterAnomalyDetectors []*clusteranomalydetection.ClusterAnomalyDetection
}

func NewService(p ServiceParam) (ServiceResult, error) {
	base := service.NewJobService("MonitoringService", service.WithDummy(p.Conf.Services.Monitoring.Dummy))
	if p.Conf.Services.Monitoring.Dummy {
		return ServiceResult{
			API:     &DummyMonitoringServiceAPI{},
			Service: base,
		}, nil
	}

	var err error
	s := &svc{
		JobService: base,

		conf:          p.Conf.Services.Monitoring,
		coreCli:       p.CoreSvcCli,
		tenantService: p.TenantService,
		msgService:    p.MsgService,
		larkCli:       p.LarkCli,
		eventbus:      p.EventBus,
	}

	// certificate checker
	s.certChecker, err = checker.NewCertificateChecker(
		s.conf.Certificate.Env,
		s.conf.Certificate.Regions,
		repo.Auth0Req{
			GrantType:    s.conf.Certificate.GrantType,
			ClientID:     s.conf.Certificate.ClientID,
			ClientSecret: s.conf.Certificate.ClientSecret,
			Audience:     s.conf.Certificate.Audience,
		},
		p.JiraClient.Raw(),
	)
	if err != nil {
		return ServiceResult{}, fmt.Errorf("failed to create certificate checker: %w", err)
	}

	// component diff checker
	s.compDiffChecker, err = checker.NewCompDiffChecker(
		s.coreCli,
		s.tenantService,
		p.Asset.Sub("monitoring", "component_diff_reports"),
		s.conf.ComponentDiffReport.ReportRetentionCount,
	)
	if err != nil {
		return ServiceResult{}, fmt.Errorf("failed to create component diff checker: %w", err)
	}

	s.weeklyOncallChecker, err = idlereminder.NewWeeklyOncallChecker(p.JiraClient, p.MsgService, p.Conf.Services.Monitoring.IdleReminder.WeeklyReport)
	if err != nil {
		return ServiceResult{}, fmt.Errorf("failed to create weekly report checker: %w", err)
	}

	// jira spec checkers
	jiraCheckers := make([]*idlereminder.JiraSpecChecker, 0)
	for _, jiraConfig := range p.Conf.Services.Monitoring.IdleReminder.JiraReport.JiraSpec {
		jiraChecker, err := idlereminder.NewJiraSpecChecker(p.JiraClient, p.MsgService, jiraConfig, p.LarkCli)
		if err != nil {
			return ServiceResult{}, fmt.Errorf("failed to created jira checker: %w", err)
		}
		jiraCheckers = append(jiraCheckers, jiraChecker)
		if !jiraConfig.CronDisable {
			job, err := service.NewCronJob(fmt.Sprintf("JiraReport-%s", jiraConfig.JQL), jiraConfig.CronSpec, jiraChecker.NotifyJiraTickets)
			if err != nil {
				return ServiceResult{}, fmt.Errorf("failed to create cron job for cleaning expired reports: %w", err)
			}
			s.AddJob(job)
		}
	}
	s.jiraSpecCheckers = jiraCheckers

	if !p.Conf.Services.Monitoring.Certificate.CronDisable {
		job, err := service.NewCronJob("CheckCertificate", s.conf.Certificate.CronSpec, s.certChecker.Check)
		if err != nil {
			return ServiceResult{}, fmt.Errorf("failed to create cron job for certificate checker: %w", err)
		}
		s.AddJob(job)
	}

	if !p.Conf.Services.Monitoring.ComponentDiffReport.CronDisable {
		job, err := service.NewCronJob("CheckComponentDiff", s.conf.ComponentDiffReport.CronSpec, s.compDiffChecker.Check)
		if err != nil {
			return ServiceResult{}, fmt.Errorf("failed to create cron job for component diff report: %w", err)
		}
		s.AddJob(job)

		job, err = service.NewCronJob("CleanExpiredReports", s.conf.ComponentDiffReport.CleanCronSpec, s.compDiffChecker.CleanReports)
		if err != nil {
			return ServiceResult{}, fmt.Errorf("failed to create cron job for cleaning expired reports: %w", err)
		}
		s.AddJob(job)
	}

	if !p.Conf.Services.Monitoring.IdleReminder.WeeklyReport.CronDisable {
		job, err := service.NewCronJob("WeeklyReport", s.conf.IdleReminder.WeeklyReport.CronSpec, s.weeklyOncallChecker.NotifyWeeklyOncallTickets)
		if err != nil {
			return ServiceResult{}, fmt.Errorf("failed to create cron job for weekly tickets checker: %w", err)
		}
		s.AddJob(job)
	}

	clusterAnomalyDetectors := make([]*clusteranomalydetection.ClusterAnomalyDetection, 0)
	if p.Conf.Services.Monitoring.ClusterAnomalyDetections != nil {
		for id, anomaly := range p.Conf.Services.Monitoring.ClusterAnomalyDetections {
			if anomaly.CronDisable {
				continue
			}
			anomalyDetector, err := clusteranomalydetection.NewClusterAnomalyDetection(
				p.MsgService, anomaly, p.ClinicCli, *p.CoreSvcCli,
			)
			if err != nil {
				return ServiceResult{}, fmt.Errorf("failed to create cluster anomaly detection: %w", err)
			}
			clusterAnomalyDetectors = append(clusterAnomalyDetectors, anomalyDetector)
			job, err := service.NewCronJob(fmt.Sprintf("ClusterAnomalyDetection-%d", id), anomaly.CronSpec, anomalyDetector.DetectAndNotify)
			if err != nil {
				return ServiceResult{}, fmt.Errorf("failed to create cron job for cluster anomaly detection: %w", err)
			}
			s.AddJob(job)
		}
	}
	s.clusterAnomalyDetectors = clusterAnomalyDetectors
	return ServiceResult{API: s, Service: s}, nil
}

type ClusterIndex struct {
	Exist bool
	Index int64
}

func (s *svc) getTopology(cluster *core.Cluster) (monent.ClusterTopology, error) {
	var topo monent.ClusterTopology

	for _, component := range cluster.Components {
		switch component.ComponentType {
		case core.ComponentType_COMPONENT_TYPE_PD:
			topo.PD = component.Replicas
		case core.ComponentType_COMPONENT_TYPE_TIDB:
			topo.TiDB = component.Replicas
		case core.ComponentType_COMPONENT_TYPE_TIKV:
			topo.TiKV = component.Replicas
		case core.ComponentType_COMPONENT_TYPE_TIFLASH:
			topo.TiFlash = component.Replicas
		default:
			return monent.ClusterTopology{}, fmt.Errorf("unsupported component type, tp=%s", component.ComponentType)
		}
	}
	return topo, nil
}

func (s *svc) ListClusterCertificates(ctx context.Context, region string) ([]*monent.Certificate, error) {
	var certificates []*monent.Certificate
	vendorAndRegion := strings.Split(region, ".")
	auth0Req := repo.Auth0Req{
		GrantType:    s.conf.Certificate.GrantType,
		Audience:     s.conf.Certificate.Audience,
		ClientID:     s.conf.Certificate.ClientID,
		ClientSecret: s.conf.Certificate.ClientSecret,
	}
	clustersResp, err := repo.QueryCluster(vendorAndRegion[1], vendorAndRegion[0], s.conf.Certificate.BizType, auth0Req)
	if err != nil {
		return nil, err
	}

	for _, c := range clustersResp.Clusters {
		clusterID := c.ApplicationID
		isDigit := unicode.IsDigit(rune(clusterID[0]))
		if !isDigit {
			continue
		}
		cert, err := s.getClusterSecretAndDetails(ctx, region, clusterID)
		if err != nil {
			if !errs.IsNotFound(err) {
				log.FromContext(ctx).Error(err, "get cluster secret failed")
			}
			continue
		}
		if len(cert.ClusterID) == 0 {
			continue
		}
		certificates = append(certificates, cert)
	}
	return certificates, nil
}

func (s *svc) getClusterSecretAndDetails(ctx context.Context, region string, clusterID string) (*monent.Certificate, error) {
	id, err := strconv.ParseUint(clusterID, 10, 64)
	if err != nil {
		return nil, err
	}
	cluster, err := s.coreCli.GetCluster(ctx, id, false)
	if err != nil {
		return nil, err
	}
	clusterCert := &monent.Certificate{}
	if cluster != nil && cluster.UserStatus.String() != "USER_CLUSTER_STATUS_DELETED" {
		tenant, err := s.tenantService.GetTenant(ctx, cluster.TenantId)
		if err != nil {
			return nil, err
		}

		bizRegion := tenant.GetTenantRegion()
		clusterCert.TenantID = fmt.Sprintf("%v", cluster.TenantId)
		clusterCert.TenantName = tenant.Name
		clusterCert.TenantAlias = tenant.Alias
		clusterCert.BizRegion = string(bizRegion)
		clusterCert.ClusterID = fmt.Sprintf("%v", cluster.Id)
		clusterCert.ClusterName = cluster.Name
		clusterCert.ClusterStatus = cluster.UserStatus.String()
		clusterCert.ProjectID = fmt.Sprintf("%v", cluster.ProjectId)

		topo, err := s.getTopology(cluster)
		if err != nil {
			return nil, err
		}
		clusterCert.Topology = topo
		certResp, err := repo.GetSecret(region, clusterID)
		if err != nil {
			return nil, err
		}
		for _, value := range certResp.Data.Result {
			clusterCert = putValue(value.Metric.Name, value.Value[1].(string), clusterCert)
		}
	}
	return clusterCert, nil
}

func (s *svc) CheckCertificate(ctx context.Context) error {
	return s.certChecker.Check(ctx)
}

func putValue(name string, value string, certificate *monent.Certificate) *monent.Certificate {
	switch name {
	case DBPdClusterSecret:
		certificate.DBPdClusterSecret = value
	case DBTikvClusterSecret:
		certificate.DBTikvClusterSecret = value
	case DBTidbClusterSecret:
		certificate.DBTidbClusterSecret = value
	case DBTiflashClusterSecret:
		certificate.DBTiflashClusterSecret = value
	case DBTidbServerSecret:
		certificate.DBTidbServerSecret = value
	case DBClusterClientSecret:
		certificate.DBClusterClientSecret = value
	case ManualDBTidbClientSecret:
		certificate.ManualDBTidbClientSecret = value
	case ProxyCertificate:
		certificate.ProxyCertificate = value
	case DBClusterCa:
		certificate.DBClusterCa = value
	case DBCdcClusterClientSecret:
		certificate.DBCdcClusterClientSecret = value
	case DBCdcPdClusterSecret:
		certificate.DBCdcPdClusterSecret = value
	}
	return certificate
}

func (s *svc) NotifyWeeklyIdleReport(ctx context.Context) error {
	return s.weeklyOncallChecker.NotifyWeeklyOncallTickets(ctx)
}

func (s *svc) NotifyJiraReport(ctx context.Context) error {
	for _, jiraChecker := range s.jiraSpecCheckers {
		err := jiraChecker.NotifyJiraTickets(ctx)
		if err != nil {
			return err
		}
	}
	return nil
}
