// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0

package cache

import (
	"fmt"
	"log/slog"
	"slices"

	"github.com/gittuf/gittuf/internal/gitinterface"
)

func (p *Persistent) GetPolicyEntries() []RSLEntryIndex {
	return p.PolicyEntries
}

func (p *Persistent) HasPolicyEntryNumber(entryNumber uint64) (gitinterface.Hash, bool) {
	if len(p.PolicyEntries) == 0 || entryNumber == 0 {
		return gitinterface.ZeroHash, false
	}

	index, has := slices.BinarySearchFunc(p.PolicyEntries, RSLEntryIndex{EntryNumber: entryNumber}, binarySearch)
	if !has {
		return gitinterface.ZeroHash, false
	}

	// Unlike Find... we're actively checking if a policy number has been
	// inserted into the cache before, so we return the ID from that index
	// exactly
	return p.PolicyEntries[index].GetEntryID(), true
}

func (p *Persistent) FindPolicyEntryNumberForEntry(entryNumber uint64) RSLEntryIndex {
	if len(p.PolicyEntries) == 0 {
		return RSLEntryIndex{EntryNumber: 0} // this is a special case
	}

	slog.Debug(fmt.Sprintf("Finding policy entry in cache before entry %d...", entryNumber))

	index, has := slices.BinarySearchFunc(p.PolicyEntries, RSLEntryIndex{EntryNumber: entryNumber}, binarySearch)
	if has {
		// The entry number given to us is the first entry which happens
		// to be the start of verification as well
		// This can happen for full verification
		return p.PolicyEntries[index]
	}

	if !has && index == 0 {
		// this happens when a policy entry doesn't exist before the specified
		// entryNumber
		return RSLEntryIndex{EntryNumber: 0}
	}

	// When !has, index is point of insertion, but we want the applicable
	// entry which is index-1
	return p.PolicyEntries[index-1]
}

func (p *Persistent) FindPolicyEntriesInRange(firstNumber, lastNumber uint64) ([]RSLEntryIndex, error) {
	if len(p.PolicyEntries) == 0 {
		return nil, ErrNoPersistentCache // TODO: check if custom error makes sense
	}

	firstIndex, has := slices.BinarySearchFunc(p.PolicyEntries, RSLEntryIndex{EntryNumber: firstNumber}, binarySearch)
	if !has {
		// When !has, index is point of insertion, but we want the applicable
		// entry which is index-1
		firstIndex--
	}

	lastIndex, has := slices.BinarySearchFunc(p.PolicyEntries, RSLEntryIndex{EntryNumber: lastNumber}, binarySearch)
	if has {
		// When has, lastIndex is an entry we want to return, so we increment
		// lastIndex to ensure the corresponding entry is included in the return
		lastIndex++
	}

	return p.PolicyEntries[firstIndex:lastIndex], nil
}

func (p *Persistent) InsertPolicyEntryNumber(entryNumber uint64, entryID gitinterface.Hash) {
	if entryNumber == 0 {
		// For now, we don't have a way to track non-numbered entries
		// We likely never want to track non-numbered entries in this
		// cache as this is very dependent on numbering
		return
	}

	// TODO: check this is for the right ref?

	slog.Debug(fmt.Sprintf("Inserting policy entry with ID '%s' and number %d into persistent cache...", entryID.String(), entryNumber))

	if len(p.PolicyEntries) == 0 {
		// No entries yet, just add the current entry
		slog.Debug("No policy entries in cache, adding current entry as sole item...")
		p.PolicyEntries = []RSLEntryIndex{{EntryNumber: entryNumber, EntryID: entryID.String()}}
		return
	}

	if p.PolicyEntries[len(p.PolicyEntries)-1].GetEntryNumber() < entryNumber {
		// Current entry clearly belongs at the very end
		slog.Debug("Current entry belongs at the end of ordered list of attestations entry, appending...")
		p.PolicyEntries = append(p.PolicyEntries, RSLEntryIndex{EntryNumber: entryNumber, EntryID: entryID.String()})
		return
	}

	// We don't check the converse where the current entry is less than the
	// first entry because we're inserting as entries are encountered
	// chronologically. Worst case, binary search fallthrough below will still
	// handle it

	slog.Debug("Searching for insertion point...")
	index, has := slices.BinarySearchFunc(p.PolicyEntries, RSLEntryIndex{EntryNumber: entryNumber}, binarySearch)
	if has {
		// We could assume that if we've seen an entry with a number greater
		// than this, we should have seen this one too, but for now...
		slog.Debug("Entry with same number found, skipping addition of entry...")
		return
	}
	slog.Debug(fmt.Sprintf("Found insertion point %d", index))

	newSlice := make([]RSLEntryIndex, 0, len(p.PolicyEntries)+1)
	newSlice = append(newSlice, p.PolicyEntries[:index]...)
	newSlice = append(newSlice, RSLEntryIndex{EntryNumber: entryNumber, EntryID: entryID.String()})
	newSlice = append(newSlice, p.PolicyEntries[index:]...)

	p.PolicyEntries = newSlice
}
