package clustermaintenance

import (
	"context"
	"fmt"
	"time"

	maintent "github.com/tidbcloud/devops/entity/maintenance"

	baseapi "github.com/tidbcloud/devops/api"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	cmxapi "github.com/tidbcloud/devops/pkg/service/clustermaintenance/api"

	"github.com/robfig/cron/v3"
)

func (s *svc) ListClusterOpsMaintenanceWindows(
	ctx context.Context,
	param cmxapi.ListClusterOpsMaintenanceWindowParam,
) ([]*maintent.ClusterOpsMaintenanceWindow, int64, error) {
	param, err := param.Normalize()
	if err != nil {
		return nil, 0, errs.APIErrorNew(baseapi.ErrCodeInvalidArgument, err)
	}

	filterOpts := make([]repoopt.Option, 0, 1)
	if len(param.ClusterIDs) > 0 {
		filterOpts = append(filterOpts, s.repo.OpsMaintenanceWindow.Options().InClusterIDs(param.ClusterIDs))
	}

	listOpts := make([]repoopt.Option, 0, len(filterOpts)+2)
	listOpts = append(listOpts, filterOpts...)
	listOpts = append(listOpts,
		&repoopt.Order{
			Column: "updated_at",
			Desc:   true,
		},
		&repoopt.Pager{
			Page:    int(param.Page),
			PerPage: int(param.PerPage),
		},
	)

	items, err := s.repo.OpsMaintenanceWindow.List(ctx, listOpts...)
	if err != nil {
		return nil, 0, err
	}
	total, err := s.repo.OpsMaintenanceWindow.Count(ctx, filterOpts...)
	if err != nil {
		return nil, 0, err
	}

	return items, total, nil
}

func (s *svc) GetClusterOpsMaintenanceWindow(
	ctx context.Context,
	clusterID string,
) (*maintent.ClusterOpsMaintenanceWindow, error) {
	if clusterID == "" {
		return nil, errs.APIErrorf(baseapi.ErrCodeInvalidArgument, "cluster id is required")
	}

	return s.repo.OpsMaintenanceWindow.Get(ctx, clusterID)
}

func (s *svc) CreateClusterOpsMaintenanceWindow(
	ctx context.Context,
	param cmxapi.CreateClusterOpsMaintenanceWindowParam,
) (*maintent.ClusterOpsMaintenanceWindow, error) {
	var window *maintent.ClusterOpsMaintenanceWindow
	var err error
	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		window, err = s.repo.OpsMaintenanceWindow.Create(ctx, &maintent.ClusterOpsMaintenanceWindow{
			ClusterID: param.ClusterID,
			Windows:   param.Windows,
			Approvers: param.Approvers,
		})
		return err
	})
	if err != nil {
		return nil, err
	}

	return window, nil
}

func (s *svc) UpdateClusterOpsMaintenanceWindow(
	ctx context.Context,
	clusterID string,
	param cmxapi.UpdateClusterOpsMaintenanceWindowParam,
) (*maintent.ClusterOpsMaintenanceWindow, error) {
	if clusterID == "" {
		return nil, errs.APIErrorf(baseapi.ErrCodeInvalidArgument, "cluster id is required")
	}

	var window *maintent.ClusterOpsMaintenanceWindow
	var err error
	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		window, err = s.repo.OpsMaintenanceWindow.Update(ctx, &maintent.ClusterOpsMaintenanceWindow{
			ClusterID: clusterID,
			Windows:   param.Windows,
			Approvers: param.Approvers,
		})
		return err
	})
	if err != nil {
		return nil, err
	}

	return window, nil
}

func (s *svc) DeleteClusterOpsMaintenanceWindow(ctx context.Context, clusterID string) error {
	if clusterID == "" {
		return errs.APIErrorf(baseapi.ErrCodeInvalidArgument, "cluster id is required")
	}

	err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
		return s.repo.OpsMaintenanceWindow.Delete(ctx, clusterID)
	})
	if err != nil {
		return fmt.Errorf("delete cluster ops maintenance window failed: %w", err)
	}

	return nil
}

func (s *svc) IsInClusterOpsMaintenanceWindow(
	ctx context.Context,
	clusterID string,
	executeTimes []time.Time,
) (bool, *maintent.ClusterOpsMaintenanceWindow, error) {
	maintenanceWindow, err := s.repo.OpsMaintenanceWindow.Get(ctx, clusterID)
	if errs.IsNotFound(err) {
		return true, nil, nil // if no maintenance window record found, treat it as always in maintenance window to be more conservative.
	}
	if err != nil {
		return false, nil, fmt.Errorf("get cluster ops maintenance window failed: %w", err)
	}

	if len(executeTimes) == 0 {
		executeTimes = []time.Time{time.Now()}
	}

	for _, executeTime := range executeTimes {
		inWindow, err := isInOpsMaintenanceWindows(executeTime, maintenanceWindow.Windows)
		if err != nil {
			return false, nil, fmt.Errorf("check cluster ops maintenance window failed: %w", err)
		}
		if !inWindow {
			return false, maintenanceWindow, nil
		}
	}

	return true, maintenanceWindow, nil
}

func isInOpsMaintenanceWindows(now time.Time, windows []maintent.ClusterOpsMaintenanceWindowSlot) (bool, error) {
	for i, window := range windows {
		sched, err := cron.ParseStandard(window.StartCronSpec)
		if err != nil {
			return false, fmt.Errorf("parse windows[%d].start_cron_spec failed: %w", i, err)
		}

		duration, err := time.ParseDuration(window.Duration)
		if err != nil {
			return false, fmt.Errorf("parse windows[%d].duration failed: %w", i, err)
		}
		if duration <= 0 {
			return false, fmt.Errorf("windows[%d].duration must be greater than 0", i)
		}

		startAt := sched.Next(now.Add(-duration))
		if !startAt.After(now) && now.Before(startAt.Add(duration)) {
			return true, nil
		}
	}

	return false, nil
}
