// Copyright 2019 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 submission contains code and structs for certificates submission proxy.
package submission

import (
	"context"
	"fmt"
	"sync"
	"time"

	ct "github.com/google/certificate-transparency-go"
	"github.com/google/certificate-transparency-go/asn1"
	"github.com/google/certificate-transparency-go/ctpolicy"
	"github.com/google/certificate-transparency-go/loglist3"
	"github.com/google/certificate-transparency-go/schedule"
	"github.com/google/certificate-transparency-go/tls"
	"github.com/google/certificate-transparency-go/x509util"
	"github.com/google/trillian/monitoring"
	"k8s.io/klog/v2"
)

// CTPolicyType indicates CT-policy used for certificate submission.
type CTPolicyType int

// Policy type values:
const (
	ChromeCTPolicy CTPolicyType = iota
	AppleCTPolicy
)

var (
	proxyOnce      sync.Once
	logListUpdates monitoring.Counter
	rspLatency     monitoring.Histogram // ep => value
)

// proxyInitMetrics initializes all the exported metrics.
func proxyInitMetrics(mf monitoring.MetricFactory) {
	logListUpdates = mf.NewCounter("log_list_updates", "Number of Log-list updates")
	rspLatency = mf.NewHistogram("http_latency", "Latency of policy-multiplexed add-responses in seconds", "ep")
}

// DistributorBuilder builds distributor instance for a given Log list.
type DistributorBuilder func(*loglist3.LogList) (*Distributor, error)

// GetDistributorBuilder given CT-policy type and Log-client builder produces
// Distributor c-tor.
func GetDistributorBuilder(plc CTPolicyType, lcBuilder LogClientBuilder, mf monitoring.MetricFactory) DistributorBuilder {
	if plc == AppleCTPolicy {
		return func(ll *loglist3.LogList) (*Distributor, error) {
			return NewDistributor(ll, ctpolicy.AppleCTPolicy{}, lcBuilder, mf)
		}
	}
	return func(ll *loglist3.LogList) (*Distributor, error) {
		return NewDistributor(ll, ctpolicy.ChromeCTPolicy{}, lcBuilder, mf)
	}
}

// ASN1MarshalSCTs serializes list of AssignedSCTs according to RFC6962 3.3
func ASN1MarshalSCTs(scts []*AssignedSCT) ([]byte, error) {
	if len(scts) == 0 {
		return nil, fmt.Errorf("ASN1MarshalSCTs requires positive number of SCTs, 0 provided")
	}
	unassignedSCTs := make([]*ct.SignedCertificateTimestamp, 0, len(scts))
	for _, sct := range scts {
		unassignedSCTs = append(unassignedSCTs, sct.SCT)
	}
	sctList, err := x509util.MarshalSCTsIntoSCTList(unassignedSCTs)
	if err != nil {
		return nil, err
	}
	encdSCTList, err := tls.Marshal(*sctList)
	if err != nil {
		return nil, err
	}
	encoded, err := asn1.Marshal(encdSCTList)
	if err != nil {
		return nil, err
	}
	return encoded, nil
}

// Proxy wraps Log List updates watcher and Distributor running on fresh Log List.
type Proxy struct {
	Init chan bool

	llRefreshInterval    time.Duration
	rootsRefreshInterval time.Duration

	llWatcher          *LogListManager
	distributorBuilder DistributorBuilder

	distMu     sync.RWMutex // guards the distributor
	dist       *Distributor
	distCancel context.CancelFunc // used to cancel distributor updates
}

// NewProxy creates an inactive Proxy instance. Call Run() to activate.
func NewProxy(llm *LogListManager, db DistributorBuilder, mf monitoring.MetricFactory) *Proxy {
	var p Proxy
	p.llWatcher = llm
	p.distributorBuilder = db
	p.Init = make(chan bool, 1)
	p.rootsRefreshInterval = 24 * time.Hour

	if mf == nil {
		mf = monitoring.InertMetricFactory{}
	}
	proxyOnce.Do(func() { proxyInitMetrics(mf) })

	return &p
}

// Run starts regular LogList checks and associated Distributor initialization.
// Sends true via Init channel when init is complete.
// Terminates upon context cancellation.
func (p *Proxy) Run(ctx context.Context, llRefresh time.Duration, rootsRefresh time.Duration) {
	init := false
	p.llRefreshInterval = llRefresh
	p.rootsRefreshInterval = rootsRefresh
	p.llWatcher.Run(ctx, llRefresh)

	go func() {
		for {
			select {
			case <-ctx.Done():
				if !init {
					close(p.Init)
				}
				return
			case llData := <-p.llWatcher.LLUpdates:
				logListUpdates.Inc()
				if err := p.restartDistributor(ctx, llData.List); err != nil {
					klog.Errorf("Unable to use Log-list:\n %v\n %v", err, llData.JSON)
				} else if !init {
					init = true
					p.Init <- true
					close(p.Init)
				}
			case err := <-p.llWatcher.Errors:
				klog.Error(err)
			}
		}
	}()
}

// restartDistributor activates new Distributor instance with Log List provided
// and sets it as active.
func (p *Proxy) restartDistributor(ctx context.Context, ll *loglist3.LogList) error {
	d, err := p.distributorBuilder(ll)
	if err != nil {
		// losing ll info. No good.
		return err
	}

	// Start refreshing roots periodically so they stay up-to-date.
	refreshCtx, refreshCancel := context.WithCancel(ctx)
	go schedule.Every(refreshCtx, p.rootsRefreshInterval, func(ectx context.Context) {
		if errs := d.RefreshRoots(ectx); len(errs) > 0 {
			for _, err := range errs {
				klog.Warning(err)
			}
		}
	})

	p.distMu.Lock()
	defer p.distMu.Unlock()
	if p.distCancel != nil {
		p.distCancel()
	}
	p.dist = d
	p.distCancel = refreshCancel
	return nil
}

// AddPreChain passes call to underlying Distributor instance.
func (p *Proxy) AddPreChain(ctx context.Context, rawChain [][]byte, loadPendingLogs bool) ([]*AssignedSCT, error) {
	if p.dist == nil {
		return []*AssignedSCT{}, fmt.Errorf("proxy distributor is not initialized. call Run()")
	}

	defer func(start time.Time) {
		rspLatency.Observe(time.Since(start).Seconds(), "add-pre-chain")
	}(time.Now())
	return p.dist.AddPreChain(ctx, rawChain, loadPendingLogs)
}

// AddChain passes call to underlying Distributor instance.
func (p *Proxy) AddChain(ctx context.Context, rawChain [][]byte, loadPendingLogs bool) ([]*AssignedSCT, error) {
	if p.dist == nil {
		return []*AssignedSCT{}, fmt.Errorf("proxy distributor is not initialized. call Run()")
	}
	defer func(start time.Time) {
		rspLatency.Observe(time.Since(start).Seconds(), "add-chain")
	}(time.Now())
	return p.dist.AddChain(ctx, rawChain, loadPendingLogs)
}
