//
// Copyright 2021 The Sigstore 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 sharding

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"

	"github.com/google/trillian"
	"github.com/google/trillian/types"
	"github.com/sigstore/rekor/pkg/log"
	"sigs.k8s.io/yaml"
)

type LogRanges struct {
	inactive Ranges
	active   int64
}

type Ranges []LogRange

type LogRange struct {
	TreeID           int64  `json:"treeID" yaml:"treeID"`
	TreeLength       int64  `json:"treeLength" yaml:"treeLength"`
	EncodedPublicKey string `json:"encodedPublicKey" yaml:"encodedPublicKey"`
	decodedPublicKey string
}

func NewLogRanges(ctx context.Context, logClient trillian.TrillianLogClient, path string, treeID uint) (LogRanges, error) {
	if path == "" {
		log.Logger.Info("No config file specified, skipping init of logRange map")
		return LogRanges{}, nil
	}
	if treeID == 0 {
		return LogRanges{}, errors.New("non-zero tlog_id required when passing in shard config filepath; please set the active tree ID via the `--trillian_log_server.tlog_id` flag")
	}
	// otherwise, try to read contents of the sharding config
	ranges, err := logRangesFromPath(path)
	if err != nil {
		return LogRanges{}, fmt.Errorf("log ranges from path: %w", err)
	}
	for i, r := range ranges {
		r, err := updateRange(ctx, logClient, r)
		if err != nil {
			return LogRanges{}, fmt.Errorf("updating range for tree id %d: %w", r.TreeID, err)
		}
		ranges[i] = r
	}
	log.Logger.Info("Ranges: %v", ranges)
	return LogRanges{
		inactive: ranges,
		active:   int64(treeID),
	}, nil
}

func logRangesFromPath(path string) (Ranges, error) {
	var ranges Ranges
	contents, err := os.ReadFile(path)
	if err != nil {
		return Ranges{}, err
	}
	if string(contents) == "" {
		log.Logger.Info("Sharding config file contents empty, skipping init of logRange map")
		return Ranges{}, nil
	}
	if err := yaml.Unmarshal(contents, &ranges); err != nil {
		// Try to use JSON
		if jerr := json.Unmarshal(contents, &ranges); jerr == nil {
			return ranges, nil
		}
		return Ranges{}, err
	}
	return ranges, nil
}

// updateRange fills in any missing information about the range
func updateRange(ctx context.Context, logClient trillian.TrillianLogClient, r LogRange) (LogRange, error) {
	// If a tree length wasn't passed in, get it ourselves
	if r.TreeLength == 0 {
		resp, err := logClient.GetLatestSignedLogRoot(ctx, &trillian.GetLatestSignedLogRootRequest{LogId: r.TreeID})
		if err != nil {
			return LogRange{}, fmt.Errorf("getting signed log root for tree %d: %w", r.TreeID, err)
		}
		var root types.LogRootV1
		if err := root.UnmarshalBinary(resp.SignedLogRoot.LogRoot); err != nil {
			return LogRange{}, err
		}
		r.TreeLength = int64(root.TreeSize)
	}
	// If a public key was provided, decode it
	if r.EncodedPublicKey != "" {
		decoded, err := base64.StdEncoding.DecodeString(r.EncodedPublicKey)
		if err != nil {
			return LogRange{}, err
		}
		r.decodedPublicKey = string(decoded)
	}
	return r, nil
}

func (l *LogRanges) ResolveVirtualIndex(index int) (int64, int64) {
	indexLeft := index
	for _, l := range l.inactive {
		if indexLeft < int(l.TreeLength) {
			return l.TreeID, int64(indexLeft)
		}
		indexLeft -= int(l.TreeLength)
	}

	// If index not found in inactive trees, return the active tree
	return l.active, int64(indexLeft)
}

func (l *LogRanges) ActiveTreeID() int64 {
	return l.active
}

func (l *LogRanges) NoInactive() bool {
	return l.inactive == nil
}

// AllShards returns all shards, starting with the active shard and then the inactive shards
func (l *LogRanges) AllShards() []int64 {
	shards := []int64{l.ActiveTreeID()}
	for _, in := range l.GetInactive() {
		shards = append(shards, in.TreeID)
	}
	return shards
}

// TotalInactiveLength returns the total length across all inactive shards;
// we don't know the length of the active shard.
func (l *LogRanges) TotalInactiveLength() int64 {
	var total int64
	for _, r := range l.inactive {
		total += r.TreeLength
	}
	return total
}

func (l *LogRanges) SetInactive(r []LogRange) {
	l.inactive = r
}

func (l *LogRanges) GetInactive() []LogRange {
	return l.inactive
}

func (l *LogRanges) AppendInactive(r LogRange) {
	l.inactive = append(l.inactive, r)
}

func (l *LogRanges) SetActive(i int64) {
	l.active = i
}

func (l *LogRanges) GetActive() int64 {
	return l.active
}

func (l *LogRanges) String() string {
	ranges := []string{}
	for _, r := range l.inactive {
		ranges = append(ranges, fmt.Sprintf("%d=%d", r.TreeID, r.TreeLength))
	}
	ranges = append(ranges, fmt.Sprintf("active=%d", l.active))
	return strings.Join(ranges, ",")
}

// PublicKey returns the associated public key for the given Tree ID
// and returns the active public key by default
func (l *LogRanges) PublicKey(activePublicKey, treeID string) (string, error) {
	// if no tree ID is specified, assume the active tree
	if treeID == "" {
		return activePublicKey, nil
	}
	tid, err := strconv.Atoi(treeID)
	if err != nil {
		return "", err
	}

	for _, i := range l.inactive {
		if int(i.TreeID) == tid {
			if i.decodedPublicKey != "" {
				return i.decodedPublicKey, nil
			}
			// assume the active public key if one wasn't provided
			return activePublicKey, nil
		}
	}
	if tid == int(l.active) {
		return activePublicKey, nil
	}
	return "", fmt.Errorf("%d is not a valid tree ID and doesn't have an associated public key", tid)
}
