package oidc

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"time"

	"code.justin.tv/amzn/TwitchS2S2/c7s"
	"code.justin.tv/amzn/TwitchS2S2/internal/interfaces"
	"code.justin.tv/amzn/TwitchS2S2/internal/opwrap"
	"code.justin.tv/amzn/TwitchS2S2/internal/s2s2err"
	"code.justin.tv/amzn/TwitchS2SJWTAlgorithms/es256"
	"code.justin.tv/video/metrics-middleware/v2/operation"
)

// ErrUnknownSigningKey is returned if a kid is requested but it is not known
type ErrUnknownSigningKey struct {
	KID string
}

func (err *ErrUnknownSigningKey) Error() string {
	return s2s2err.S2S2ErrorString("unknown signing key: "+err.KID, "UnknownSigningKey")
}

// OIDC implements the meta data endpoints for OIDC
type OIDC struct {
	Client           interfaces.HTTPClient
	Config           *c7s.Config
	OperationStarter *operation.Starter

	oidcConfig *Configuration
}

// Init initializes this client. This MUST be run.
func (o *OIDC) Init(ctx context.Context) error {
	return o.init(ctx)
}

func (o *OIDC) init(ctx context.Context) (err error) {
	ctx, op := o.OperationStarter.StartOp(ctx, opwrap.GetOIDCConfiguration)
	defer opwrap.EndWithError(op, err)

	req, err := http.NewRequest("GET", o.Config.DiscoveryEndpoint, nil)
	if err != nil {
		return err
	}
	req = req.WithContext(ctx)

	res, err := o.Client.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		bs, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return err
		}
		return fmt.Errorf("error reading from discovery endpoint: %s", string(bs))
	}

	return json.NewDecoder(res.Body).Decode(&o.oidcConfig)
}

// Configuration returns the OIDC configuration
func (o *OIDC) Configuration() *Configuration {
	return o.oidcConfig
}

// ValidationKeys returns all validation keys
func (o *OIDC) ValidationKeys(ctx context.Context) (_ map[string]ValidationKey, _ time.Duration, err error) {
	ctx, op := o.OperationStarter.StartOp(ctx, opwrap.GetOIDCValidationKeys)
	defer opwrap.EndWithError(op, err)

	req, err := http.NewRequest("GET", o.Configuration().JwksURI, nil)
	if err != nil {
		return nil, 0, err
	}

	res, err := o.Client.Do(req.WithContext(ctx))
	if err != nil {
		return nil, 0, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		bs, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return nil, 0, err
		}
		return nil, 0, fmt.Errorf("error getting validation keys: %s", string(bs))
	}

	maxAge, err := parseCacheControlMaxAge(res)
	if err != nil {
		return nil, 0, err
	}

	var raw struct {
		Keys []*es256.PublicKey `json:"keys"`
	}
	if err := json.NewDecoder(res.Body).Decode(&raw); err != nil {
		return nil, 0, err
	}

	out := make(map[string]ValidationKey, len(raw.Keys))
	for _, v := range raw.Keys {
		out[v.KeyID] = v
	}

	return out, maxAge, nil
}

// ValidationKey returns the validation key by ID from cache.
// See oidccache/cache.go for implementation.
func (o *OIDC) ValidationKey(_ context.Context, _ string) (ValidationKey, error) {
	return nil, errors.New("not implemented")
}
