// Copyright 2018 Google LLC. All Rights Reserved.
//
// 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 integration

import (
	"context"
	"crypto/sha256"
	"errors"
	"fmt"
	"math/rand"
	"time"

	"github.com/google/certificate-transparency-go/client"
	"github.com/google/certificate-transparency-go/scanner"
	"github.com/google/certificate-transparency-go/trillian/ctfe/configpb"
	"github.com/google/certificate-transparency-go/x509"
	"github.com/google/certificate-transparency-go/x509util"
	"k8s.io/klog/v2"

	ct "github.com/google/certificate-transparency-go"
)

// CopyChainGenerator creates certificate chains by copying suitable examples
// from a source log.
type CopyChainGenerator struct {
	start, limit             time.Time
	sourceRoots, targetRoots *x509util.PEMCertPool
	certs, precerts          chan []ct.ASN1Cert
}

// CopyChainOptions describes the parameters for a CopyChainGenerator instance.
type CopyChainOptions struct {
	// StartIndex indicates where to start scanning; negative value implies starting from a random position.
	StartIndex int64
	// BufSize is the number of buffered chains to hold.
	BufSize int
	// BatchSize indicates how many entries should be requested from the source log at a time.
	BatchSize int
	// ParallelFetch indicates how many parallel entry fetchers to run.
	ParallelFetch int
}

// NewCopyChainGenerator builds a certificate chain generator that sources
// chains from another source log, starting at startIndex (or a random index in
// the current tree size if startIndex is negative).  This function starts
// background goroutines that scan the log; cancelling the context will
// terminate these goroutines (after that the [Pre]CertChain() entrypoints will
// permanently fail).
func NewCopyChainGenerator(ctx context.Context, client *client.LogClient, cfg *configpb.LogConfig, startIndex int64, bufSize int) (ChainGenerator, error) {
	opts := CopyChainOptions{
		StartIndex:    startIndex,
		BufSize:       bufSize,
		BatchSize:     500,
		ParallelFetch: 2,
	}
	return NewCopyChainGeneratorFromOpts(ctx, client, cfg, opts)
}

// NewCopyChainGeneratorFromOpts builds a certificate chain generator that
// sources chains from another source log, starting at opts.StartIndex (or a
// random index in the current tree size if this is negative).  This function
// starts background goroutines that scan the log; cancelling the context will
// terminate these goroutines (after that the [Pre]CertChain() entrypoints
// will permanently fail).
func NewCopyChainGeneratorFromOpts(ctx context.Context, client *client.LogClient, cfg *configpb.LogConfig, opts CopyChainOptions) (ChainGenerator, error) {
	var start, limit time.Time
	var err error
	if cfg.NotAfterStart != nil {
		if err := cfg.NotAfterStart.CheckValid(); err != nil {
			return nil, fmt.Errorf("failed to parse NotAfterStart: %v", err)
		}
		start = cfg.NotAfterStart.AsTime()
	}
	if cfg.NotAfterLimit != nil {
		if err := cfg.NotAfterLimit.CheckValid(); err != nil {
			return nil, fmt.Errorf("failed to parse NotAfterLimit: %v", err)
		}
		limit = cfg.NotAfterLimit.AsTime()
	}

	targetPool := x509util.NewPEMCertPool()
	for _, pemFile := range cfg.RootsPemFile {
		if err := targetPool.AppendCertsFromPEMFile(pemFile); err != nil {
			return nil, fmt.Errorf("failed to read trusted roots for target log: %v", err)
		}
	}
	if klog.V(4).Enabled() {
		for _, cert := range targetPool.RawCertificates() {
			klog.Infof("target root cert: %x Subject: %v", sha256.Sum256(cert.Raw), cert.Subject)
		}
	}

	seenOverlap := false
	srcRoots, err := client.GetAcceptedRoots(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to read trusted roots for source log: %v", err)
	}
	sourcePool := x509util.NewPEMCertPool()
	for _, root := range srcRoots {
		cert, err := x509.ParseCertificate(root.Data)
		if x509.IsFatal(err) {
			klog.Warningf("Failed to parse root certificate from source log: %v", err)
			continue
		}
		klog.V(4).Infof("source log root cert: %x Subject: %v", sha256.Sum256(cert.Raw), cert.Subject)
		sourcePool.AddCert(cert)
		if targetPool.Included(cert) {
			klog.V(3).Infof("source log root cert is accepted by target: %x Subject: %v", sha256.Sum256(cert.Raw), cert.Subject)
			seenOverlap = true
		}
	}
	if !seenOverlap {
		return nil, fmt.Errorf("failed to find any overlap in accepted roots for target %s", cfg.Prefix)
	}

	startIndex := opts.StartIndex
	if startIndex < 0 {
		// Pick a random start point in the source log's tree.
		sth, err := client.GetSTH(ctx)
		if err != nil {
			return nil, fmt.Errorf("failed to get STH for source log: %v", err)
		}
		startIndex = rand.Int63n(int64(sth.TreeSize))
		klog.Infof("starting CopyChainGenerator from index %d (of %d) in source log", startIndex, sth.TreeSize)
	}

	generator := &CopyChainGenerator{
		start:       start,
		limit:       limit,
		targetRoots: targetPool,
		sourceRoots: sourcePool,
		certs:       make(chan []ct.ASN1Cert, opts.BufSize),
		precerts:    make(chan []ct.ASN1Cert, opts.BufSize),
	}

	// Start two goroutines to scan the source log for certs and precerts respectively.
	fetchOpts := scanner.FetcherOptions{
		BatchSize:     opts.BatchSize,
		ParallelFetch: opts.ParallelFetch,
		Continuous:    true,
		StartIndex:    startIndex,
	}
	go func() {
		certFetcher := scanner.NewFetcher(client, &fetchOpts)
		if err := certFetcher.Run(ctx, func(batch scanner.EntryBatch) {
			generator.processBatch(batch, generator.certs, ct.X509LogEntryType)
		}); err != nil {
			klog.Errorf("processBatch(): %v", err)
		}
	}()
	go func() {
		precertFetcher := scanner.NewFetcher(client, &fetchOpts)
		if err := precertFetcher.Run(ctx, func(batch scanner.EntryBatch) {
			generator.processBatch(batch, generator.precerts, ct.PrecertLogEntryType)
		}); err != nil {
			klog.Errorf("processBatch(): %v", err)
		}
	}()

	return generator, nil
}

// processBatch extracts chains of the desired type from a batch of entries and sends
// them down the channel.  May block on the channel consumer.
func (g *CopyChainGenerator) processBatch(batch scanner.EntryBatch, chains chan []ct.ASN1Cert, eType ct.LogEntryType) {
	klog.V(2).Infof("processBatch(%d): examine batch [%d, %d)", eType, batch.Start, int(batch.Start)+len(batch.Entries))
	for i, entry := range batch.Entries {
		index := batch.Start + int64(i)
		entry, err := ct.RawLogEntryFromLeaf(index, &entry)
		if err != nil {
			klog.Errorf("processBatch(%d): failed to build raw log entry %d: %v", eType, index, err)
			continue
		}
		if entry.Leaf.TimestampedEntry.EntryType != eType {
			klog.V(4).Infof("skip entry %d as EntryType=%d not %d", index, entry.Leaf.TimestampedEntry.EntryType, eType)
			continue
		}
		root, err := x509.ParseCertificate(entry.Chain[len(entry.Chain)-1].Data)
		if err != nil {
			klog.V(3).Infof("skip entry %d as its root cannot be parsed to check accepted: %v", index, err)
			continue
		}
		if !g.targetRoots.Included(root) {
			klog.V(3).Infof("skip entry %d as its root is not accepted by target log", index)
			continue
		}
		if !g.start.IsZero() || !g.limit.IsZero() {
			// Target log has NotAfter boundaries, so we need to parse the leaf cert to check
			// whether it complies with them.
			cert, err := x509.ParseCertificate(entry.Cert.Data)
			if x509.IsFatal(err) {
				klog.V(3).Infof("skip entry %d as its leaf cannot be parsed to check NotAfter: %v", index, err)
				continue
			}
			if !g.start.IsZero() && cert.NotAfter.Before(g.start) {
				klog.V(3).Infof("skip entry %d as its NotAfter (%v) is before %v", index, cert.NotAfter, g.start)
				continue
			}
			if !g.limit.IsZero() && !cert.NotAfter.Before(g.limit) {
				klog.V(3).Infof("skip entry %d as its NotAfter (%v) is after %v", index, cert.NotAfter, g.limit)
				continue
			}
		}

		chain := make([]ct.ASN1Cert, len(entry.Chain)+1)
		chain[0] = entry.Cert
		copy(chain[1:], entry.Chain)
		chains <- chain
	}
}

// CertChain returns a new cert chain taken from the source log.
// This may block until a suitable cert has been discovered in the source log.
func (g *CopyChainGenerator) CertChain() ([]ct.ASN1Cert, error) {
	chain := <-g.certs
	if len(chain) == 0 {
		return nil, errors.New("no certs available")
	}
	return chain, nil
}

// PreCertChain returns a new precert chain taken from the source log.
// This may block until a suitable precert has been discovered in the source log.
func (g *CopyChainGenerator) PreCertChain() ([]ct.ASN1Cert, []byte, error) {
	prechain := <-g.precerts
	if len(prechain) == 0 {
		return nil, nil, errors.New("no precerts available")
	}
	tbs, err := buildLeafTBS(prechain[0].Data, nil)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to build leaf TBSCertificate: %v", err)
	}
	return prechain, tbs, nil
}
