// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package mgt

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"github.com/gocraft/work"
	"github.com/gomodule/redigo/redis"

	"github.com/goharbor/harbor/src/jobservice/common/query"
	"github.com/goharbor/harbor/src/jobservice/common/rds"
	"github.com/goharbor/harbor/src/jobservice/common/utils"
	"github.com/goharbor/harbor/src/jobservice/errs"
	"github.com/goharbor/harbor/src/jobservice/job"
	"github.com/goharbor/harbor/src/jobservice/logger"
	"github.com/goharbor/harbor/src/jobservice/period"
	"github.com/goharbor/harbor/src/lib/errors"
)

// Manager defies the related operations to handle the management of job stats.
type Manager interface {
	// Get the stats data of all kinds of jobs.
	// Data returned by pagination.
	//
	// Arguments:
	//   q *query.Parameter : the query parameters
	//
	// Returns:
	//   The matched job stats list
	//   The total number of the jobs
	//   Non nil error if any issues meet
	GetJobs(q *query.Parameter) ([]*job.Stats, int64, error)

	// Get the executions of the specified periodic job by pagination
	//
	// Arguments:
	//   pID: ID of the periodic job
	//   q *query.Parameter: query parameters
	//
	// Returns:
	//   The matched job stats list,
	//   The total number of the executions,
	//   Non nil error if any issues meet.
	GetPeriodicExecution(pID string, q *query.Parameter) ([]*job.Stats, int64, error)

	// Get the scheduled jobs
	//
	// Arguments:
	//   q *query.Parameter: query parameters
	//
	// Returns:
	//   The matched job stats list,
	//   The total number of the executions,
	//   Non nil error if any issues meet.
	GetScheduledJobs(q *query.Parameter) ([]*job.Stats, int64, error)

	// Get the stats of the specified job
	//
	// Arguments:
	//   jobID string: ID of the job
	//
	// Returns:
	//   The job stats
	//   Non nil error if any issues meet
	GetJob(jobID string) (*job.Stats, error)

	// Save the job stats
	//
	// Arguments:
	//   job *job.Stats: the saving job stats
	//
	// Returns:
	//   Non nil error if any issues meet
	SaveJob(job *job.Stats) error
}

// basicManager is the default implementation of @manager,
// based on redis.
type basicManager struct {
	// system context
	ctx context.Context
	// db namespace
	namespace string
	// redis conn pool
	pool *redis.Pool
	// go work client
	client *work.Client
}

// NewManager news a basic manager
func NewManager(ctx context.Context, ns string, pool *redis.Pool) Manager {
	return &basicManager{
		ctx:       ctx,
		namespace: ns,
		pool:      pool,
		client:    work.NewClient(ns, pool),
	}
}

// GetJobs is implementation of Manager.GetJobs
// Because of the hash set used to keep the job stats, we can not support a standard pagination.
// A cursor is used to fetch the jobs with several batches.
func (bm *basicManager) GetJobs(q *query.Parameter) ([]*job.Stats, int64, error) {
	cursor, count := int64(0), query.DefaultPageSize
	if q != nil {
		if q.PageSize > 0 {
			count = q.PageSize
		}

		if cur, ok := q.Extras.Get(query.ExtraParamKeyCursor); ok {
			cursor = cur.(int64)
		}
	}

	pattern := rds.KeyJobStats(bm.namespace, "*")
	args := []interface{}{cursor, "MATCH", pattern, "COUNT", count}

	conn := bm.pool.Get()
	defer func() {
		_ = conn.Close()
	}()

	values, err := redis.Values(conn.Do("SCAN", args...))
	if err != nil {
		return nil, 0, err
	}
	if len(values) != 2 {
		return nil, 0, errors.New("malform scan results")
	}

	nextCur, err := strconv.ParseInt(string(values[0].([]byte)), 10, 64)
	if err != nil {
		return nil, 0, err
	}
	list := values[1].([]interface{})

	results := make([]*job.Stats, 0)
	for _, v := range list {
		if bytes, ok := v.([]byte); ok {
			statsKey := string(bytes)
			if i := strings.LastIndex(statsKey, ":"); i != -1 {
				jID := statsKey[i+1:]
				t := job.NewBasicTrackerWithID(bm.ctx, jID, bm.namespace, bm.pool, nil, nil)
				if err := t.Load(); err != nil {
					logger.Errorf("retrieve stats data of job %s error: %s", jID, err)
					continue
				}

				results = append(results, t.Job())
			}
		}
	}

	return results, nextCur, nil
}

// GetPeriodicExecution is implementation of Manager.GetPeriodicExecution
func (bm *basicManager) GetPeriodicExecution(pID string, q *query.Parameter) (results []*job.Stats, total int64, err error) {
	if utils.IsEmptyStr(pID) {
		return nil, 0, errors.New("nil periodic job ID")
	}

	tracker := job.NewBasicTrackerWithID(bm.ctx, pID, bm.namespace, bm.pool, nil, nil)
	err = tracker.Load()
	if err != nil {
		return nil, 0, err
	}

	if tracker.Job().Info.JobKind != job.KindPeriodic {
		return nil, 0, errors.Errorf("only periodic job has executions: %s kind is received", tracker.Job().Info.JobKind)
	}

	conn := bm.pool.Get()
	defer func() {
		_ = conn.Close()
	}()

	key := rds.KeyUpstreamJobAndExecutions(bm.namespace, pID)

	executionIDs := make([]string, 0)
	// Query executions by "non stopped"
	if nonStoppedOnly, ok := q.Extras.Get(query.ExtraParamKeyNonStoppedOnly); ok {
		if v, yes := nonStoppedOnly.(bool); yes && v {
			executionIDs, total, err = queryExecutions(conn, key, q)
			if err != nil {
				return nil, 0, err
			}
		}
	} else {
		// Query all
		// Pagination
		var pageNumber, pageSize uint = 1, query.DefaultPageSize
		if q != nil {
			if q.PageNumber > 0 {
				pageNumber = q.PageNumber
			}
			if q.PageSize > 0 {
				pageSize = q.PageSize
			}
		}

		// Get total first
		total, err = redis.Int64(conn.Do("ZCARD", key))
		if err != nil {
			return nil, 0, err
		}

		// No items
		if total == 0 || (int64)((pageNumber-1)*pageSize) >= total {
			return results, total, nil
		}

		min, max := (pageNumber-1)*pageSize, pageNumber*pageSize-1
		args := []interface{}{key, min, max}
		list, err := redis.Values(conn.Do("ZREVRANGE", args...))
		if err != nil {
			return nil, 0, err
		}

		for _, item := range list {
			if eID, ok := item.([]byte); ok {
				executionIDs = append(executionIDs, string(eID))
			}
		}
	}

	for _, eID := range executionIDs {
		t := job.NewBasicTrackerWithID(bm.ctx, eID, bm.namespace, bm.pool, nil, nil)
		if er := t.Load(); er != nil {
			logger.Errorf("track job %s error: %s", eID, err)
			continue
		}

		results = append(results, t.Job())
	}

	return
}

// GetScheduledJobs is implementation of Manager.GetScheduledJobs
func (bm *basicManager) GetScheduledJobs(q *query.Parameter) ([]*job.Stats, int64, error) {
	// PageSize is not supported here
	var page uint = 1
	if q != nil && q.PageNumber > 1 {
		page = q.PageNumber
	}

	sJobs, total, err := bm.client.ScheduledJobs(page)
	if err != nil {
		return nil, 0, err
	}

	res := make([]*job.Stats, 0)
	for _, sJob := range sJobs {
		jID := sJob.ID
		if len(sJob.Args) > 0 {
			if _, ok := sJob.Args[period.PeriodicExecutionMark]; ok {
				// Periodic scheduled job
				jID = fmt.Sprintf("%s@%d", sJob.ID, sJob.RunAt)
			}
		}
		t := job.NewBasicTrackerWithID(bm.ctx, jID, bm.namespace, bm.pool, nil, nil)
		err = t.Load()
		if err != nil {
			// Just log it
			logger.Errorf("mgt.basicManager: query scheduled jobs error: %s", err)
			continue
		}

		res = append(res, t.Job())
	}

	return res, total, nil
}

// GetJob is implementation of Manager.GetJob
func (bm *basicManager) GetJob(jobID string) (*job.Stats, error) {
	if utils.IsEmptyStr(jobID) {
		return nil, errs.BadRequestError("empty job ID")
	}

	t := job.NewBasicTrackerWithID(bm.ctx, jobID, bm.namespace, bm.pool, nil, nil)
	if err := t.Load(); err != nil {
		return nil, err
	}

	return t.Job(), nil
}

// SaveJob is implementation of Manager.SaveJob
func (bm *basicManager) SaveJob(j *job.Stats) error {
	if j == nil {
		return errs.BadRequestError("nil saving job stats")
	}

	t := job.NewBasicTrackerWithStats(bm.ctx, j, bm.namespace, bm.pool, nil, nil)
	return t.Save()
}

// queryExecutions queries periodic executions by status
func queryExecutions(conn redis.Conn, dataKey string, q *query.Parameter) ([]string, int64, error) {
	total, err := redis.Int64(conn.Do("ZCOUNT", dataKey, 0, "+inf"))
	if err != nil {
		return nil, 0, err
	}

	var pageNumber, pageSize uint = 1, query.DefaultPageSize
	if q.PageNumber > 0 {
		pageNumber = q.PageNumber
	}
	if q.PageSize > 0 {
		pageSize = q.PageSize
	}

	results := make([]string, 0)
	if total == 0 || (int64)((pageNumber-1)*pageSize) >= total {
		return results, total, nil
	}

	offset := (pageNumber - 1) * pageSize
	args := []interface{}{dataKey, "+inf", 0, "LIMIT", offset, pageSize}

	eIDs, err := redis.Values(conn.Do("ZREVRANGEBYSCORE", args...))
	if err != nil {
		return nil, 0, err
	}

	for _, eID := range eIDs {
		if eIDBytes, ok := eID.([]byte); ok {
			results = append(results, string(eIDBytes))
		}
	}

	return results, total, nil
}
