// Copyright (c) 2019, Google Inc.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

package acvp

import (
	"bytes"
	"crypto"
	"crypto/tls"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"reflect"
	"strings"
	"time"
)

const loginEndpoint = "acvp/v1/login"

// Server represents an ACVP server.
type Server struct {
	// PrefixTokens are access tokens that apply to URLs under a certain prefix.
	// The keys of this map are strings like "acvp/v1/testSessions/1234" and the
	// values are JWT access tokens.
	PrefixTokens map[string]string
	// SizeLimit is the maximum number of bytes that the server can accept
	// as an upload before the large endpoint support must be used. Zero
	// means that there is no limit.
	SizeLimit uint64
	// AccessToken is the top-level access token for the current session.
	AccessToken string

	client   *http.Client
	prefix   string
	totpFunc func() string
}

// NewServer returns a fresh Server instance representing the ACVP server at
// prefix (e.g. "https://acvp.example.com/"). A copy of all bytes exchanged
// will be written to logFile, if not empty.
func NewServer(prefix string, logFile string, derCertificates [][]byte, privateKey crypto.PrivateKey, totp func() string) *Server {
	if !strings.HasSuffix(prefix, "/") {
		prefix = prefix + "/"
	}

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{
			tls.Certificate{
				Certificate: derCertificates,
				PrivateKey:  privateKey,
			},
		},
		Renegotiation: tls.RenegotiateOnceAsClient,
	}

	client := &http.Client{
		Transport: &http.Transport{
			Dial: func(network, addr string) (net.Conn, error) {
				panic("HTTP connection requested")
			},
			DialTLS: func(network, addr string) (net.Conn, error) {
				conn, err := tls.Dial(network, addr, tlsConfig)
				if err != nil {
					return nil, err
				}
				if len(logFile) > 0 {
					logFile, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
					if err != nil {
						return nil, err
					}
					return &logger{Conn: conn, log: logFile}, nil
				}
				return conn, err
			},
		},
		Timeout: 120 * time.Second,
	}

	return &Server{client: client, prefix: prefix, totpFunc: totp, PrefixTokens: make(map[string]string)}
}

type logger struct {
	*tls.Conn
	log           *os.File
	lastDirection int
}

var newLine = []byte{'\n'}

func (l *logger) Read(buf []byte) (int, error) {
	if l.lastDirection != 1 {
		l.log.Write(newLine)
	}
	l.lastDirection = 1

	n, err := l.Conn.Read(buf)
	if err == nil {
		l.log.Write(buf[:n])
	}
	return n, err
}

func (l *logger) Write(buf []byte) (int, error) {
	if l.lastDirection != 2 {
		l.log.Write(newLine)
	}
	l.lastDirection = 2

	n, err := l.Conn.Write(buf)
	if err == nil {
		l.log.Write(buf[:n])
	}
	return n, err
}

const requestPrefix = `[{"acvVersion":"1.0"},`
const requestSuffix = "]"

// parseHeaderElement parses the first JSON object that's always returned by
// ACVP servers. If successful, it returns a JSON Decoder positioned just
// before the second element.
func parseHeaderElement(in io.Reader) (*json.Decoder, error) {
	decoder := json.NewDecoder(in)
	arrayStart, err := decoder.Token()
	if err != nil {
		return nil, errors.New("failed to read from server reply: " + err.Error())
	}
	if delim, ok := arrayStart.(json.Delim); !ok || delim != '[' {
		return nil, fmt.Errorf("found %#v when expecting initial array from server", arrayStart)
	}

	var version struct {
		Version string `json:"acvVersion"`
	}
	if err := decoder.Decode(&version); err != nil {
		return nil, errors.New("parse error while decoding version element: " + err.Error())
	}
	if !strings.HasPrefix(version.Version, "1.") {
		return nil, fmt.Errorf("expected version 1.* from server but found %q", version.Version)
	}

	return decoder, nil
}

// parseReplyToBytes reads the contents of an ACVP reply after removing the
// header element.
func parseReplyToBytes(in io.Reader) ([]byte, error) {
	decoder, err := parseHeaderElement(in)
	if err != nil {
		return nil, err
	}

	buf, err := io.ReadAll(decoder.Buffered())
	if err != nil {
		return nil, err
	}

	rest, err := io.ReadAll(in)
	if err != nil {
		return nil, err
	}
	buf = append(buf, rest...)

	buf = bytes.TrimSpace(buf)
	if len(buf) == 0 || buf[0] != ',' {
		return nil, errors.New("didn't find initial ','")
	}
	buf = buf[1:]

	if len(buf) == 0 || buf[len(buf)-1] != ']' {
		return nil, errors.New("didn't find trailing ']'")
	}
	buf = buf[:len(buf)-1]

	return buf, nil
}

// parseReply parses the contents of an ACVP reply (after removing the header
// element) into out. See the documentation of the encoding/json package for
// details of the parsing.
func parseReply(out any, in io.Reader) error {
	if out == nil {
		// No reply expected.
		return nil
	}

	decoder, err := parseHeaderElement(in)
	if err != nil {
		return err
	}

	if err := decoder.Decode(out); err != nil {
		return errors.New("error while decoding reply body: " + err.Error())
	}

	arrayEnd, err := decoder.Token()
	if err != nil {
		return errors.New("failed to read end of reply from server: " + err.Error())
	}
	if delim, ok := arrayEnd.(json.Delim); !ok || delim != ']' {
		return fmt.Errorf("found %#v when expecting end of array from server", arrayEnd)
	}
	if decoder.More() {
		return errors.New("unexpected trailing data from server")
	}

	return nil
}

// expired returns true if the given JWT token has expired.
func expired(tokenStr string) bool {
	parts := strings.Split(tokenStr, ".")
	if len(parts) != 3 {
		return false
	}
	jsonBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return false
	}
	var token struct {
		Expiry uint64 `json:"exp"`
	}
	if json.Unmarshal(jsonBytes, &token) != nil {
		return false
	}
	return token.Expiry > 0 && token.Expiry < uint64(time.Now().Add(-10*time.Second).Unix())
}

func (server *Server) getToken(endPoint string) (string, error) {
	for path, token := range server.PrefixTokens {
		if endPoint != path && !strings.HasPrefix(endPoint, path+"/") {
			continue
		}

		if !expired(token) {
			return token, nil
		}

		var reply struct {
			AccessToken string `json:"accessToken"`
		}
		if err := server.postMessage(&reply, loginEndpoint, map[string]string{
			"password":    server.totpFunc(),
			"accessToken": token,
		}); err != nil {
			return "", err
		}
		server.PrefixTokens[path] = reply.AccessToken
		return reply.AccessToken, nil
	}
	return server.AccessToken, nil
}

// Login sends a login request and stores the returned access tokens for use
// with future requests. The login process isn't specifically documented in
// draft-fussell-acvp-spec and the best reference is
// https://github.com/usnistgov/ACVP/wiki#credentials-for-accessing-the-demo-server
func (server *Server) Login() error {
	var reply struct {
		AccessToken           string `json:"accessToken"`
		LargeEndpointRequired bool   `json:"largeEndpointRequired"`
		SizeLimit             int64  `json:"sizeConstraint"`
	}

	if err := server.postMessage(&reply, loginEndpoint, map[string]string{"password": server.totpFunc()}); err != nil {
		return err
	}

	if len(reply.AccessToken) == 0 {
		return errors.New("login reply didn't contain access token")
	}
	server.AccessToken = reply.AccessToken

	if reply.LargeEndpointRequired {
		if reply.SizeLimit <= 0 {
			return errors.New("login indicated largeEndpointRequired but didn't provide a sizeConstraint")
		}
		server.SizeLimit = uint64(reply.SizeLimit)
	}

	return nil
}

type Relation int

const (
	Equals           Relation = iota
	NotEquals        Relation = iota
	GreaterThan      Relation = iota
	GreaterThanEqual Relation = iota
	LessThan         Relation = iota
	LessThanEqual    Relation = iota
	Contains         Relation = iota
	StartsWith       Relation = iota
	EndsWith         Relation = iota
)

func (rel Relation) String() string {
	switch rel {
	case Equals:
		return "eq"
	case NotEquals:
		return "ne"
	case GreaterThan:
		return "gt"
	case GreaterThanEqual:
		return "ge"
	case LessThan:
		return "lt"
	case LessThanEqual:
		return "le"
	case Contains:
		return "contains"
	case StartsWith:
		return "start"
	case EndsWith:
		return "end"
	default:
		panic("unknown relation")
	}
}

type Condition struct {
	Param    string
	Relation Relation
	Value    string
}

type Conjunction []Condition

type Query []Conjunction

func (query Query) toURLParams() string {
	var ret string

	for i, conj := range query {
		for _, cond := range conj {
			if len(ret) > 0 {
				ret += "&"
			}
			ret += fmt.Sprintf("%s[%d]=%s:%s", url.QueryEscape(cond.Param), i, cond.Relation.String(), url.QueryEscape(cond.Value))
		}
	}

	return ret
}

var NotFound = errors.New("acvp: HTTP code 404")

func (server *Server) newRequestWithToken(method, endpoint string, body io.Reader) (*http.Request, error) {
	token, err := server.getToken(endpoint)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequest(method, server.prefix+endpoint, body)
	if err != nil {
		return nil, err
	}
	if len(token) != 0 && endpoint != loginEndpoint {
		req.Header.Add("Authorization", "Bearer "+token)
	}
	return req, nil
}

func (server *Server) Get(out any, endPoint string) error {
	req, err := server.newRequestWithToken("GET", endPoint, nil)
	if err != nil {
		return err
	}
	resp, err := server.client.Do(req)
	if err != nil {
		return fmt.Errorf("error while fetching chunk for %q: %s", endPoint, err)
	}

	defer resp.Body.Close()
	if resp.StatusCode == 404 {
		return NotFound
	} else if resp.StatusCode != 200 {
		return fmt.Errorf("acvp: HTTP error %d", resp.StatusCode)
	}
	return parseReply(out, resp.Body)
}

func (server *Server) GetBytes(endPoint string) ([]byte, error) {
	req, err := server.newRequestWithToken("GET", endPoint, nil)
	if err != nil {
		return nil, err
	}
	resp, err := server.client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("error while fetching chunk for %q: %s", endPoint, err)
	}

	defer resp.Body.Close()
	if resp.StatusCode == 404 {
		return nil, NotFound
	} else if resp.StatusCode != 200 {
		return nil, fmt.Errorf("acvp: HTTP error %d", resp.StatusCode)
	}
	return parseReplyToBytes(resp.Body)
}

func (server *Server) write(method string, reply any, endPoint string, contents []byte) error {
	var buf bytes.Buffer
	buf.WriteString(requestPrefix)
	buf.Write(contents)
	buf.WriteString(requestSuffix)

	req, err := server.newRequestWithToken(method, endPoint, &buf)
	if err != nil {
		return err
	}
	req.Header.Add("Content-Type", "application/json")
	resp, err := server.client.Do(req)
	if err != nil {
		return fmt.Errorf("error while writing to %q: %s", endPoint, err)
	}

	defer resp.Body.Close()
	if resp.StatusCode == 404 {
		return NotFound
	} else if resp.StatusCode != 200 {
		return fmt.Errorf("acvp: HTTP error %d", resp.StatusCode)
	}
	return parseReply(reply, resp.Body)
}

func (server *Server) postMessage(reply any, endPoint string, request any) error {
	contents, err := json.Marshal(request)
	if err != nil {
		return err
	}
	return server.write("POST", reply, endPoint, contents)
}

func (server *Server) Post(out any, endPoint string, contents []byte) error {
	return server.write("POST", out, endPoint, contents)
}

func (server *Server) Put(out any, endPoint string, contents []byte) error {
	return server.write("PUT", out, endPoint, contents)
}

func (server *Server) Delete(endPoint string) error {
	req, err := server.newRequestWithToken("DELETE", endPoint, nil)
	resp, err := server.client.Do(req)
	if err != nil {
		return fmt.Errorf("error while writing to %q: %s", endPoint, err)
	}

	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return fmt.Errorf("acvp: HTTP error %d", resp.StatusCode)
	}
	fmt.Printf("DELETE %q %d\n", server.prefix+endPoint, resp.StatusCode)
	return nil
}

var (
	uint64Type = reflect.TypeOf(uint64(0))
	boolType   = reflect.TypeOf(false)
	stringType = reflect.TypeOf("")
)

// GetPaged returns an array of records of some type using one or more requests to the server. See
// https://pages.nist.gov/ACVP/draft-fussell-acvp-spec.html#paging_response
func (server *Server) GetPaged(out any, endPoint string, condition Query) error {
	output := reflect.ValueOf(out)
	if output.Kind() != reflect.Ptr {
		panic(fmt.Sprintf("GetPaged output parameter of non-pointer type %T", out))
	}

	token, err := server.getToken(endPoint)
	if err != nil {
		return err
	}

	outputSlice := output.Elem()

	replyType := reflect.StructOf([]reflect.StructField{
		{Name: "TotalCount", Type: uint64Type, Tag: `json:"totalCount"`},
		{Name: "Incomplete", Type: boolType, Tag: `json:"incomplete"`},
		{Name: "Data", Type: output.Elem().Type(), Tag: `json:"data"`},
		{Name: "Links", Type: reflect.StructOf([]reflect.StructField{
			{Name: "Next", Type: stringType, Tag: `json:"next"`},
		}), Tag: `json:"links"`},
	})
	nextURL := server.prefix + endPoint
	conditionParams := condition.toURLParams()
	if len(conditionParams) > 0 {
		nextURL += "?" + conditionParams
	}

	isFirstRequest := true
	for {
		req, err := http.NewRequest("GET", nextURL, nil)
		if err != nil {
			return err
		}
		if len(token) != 0 {
			req.Header.Add("Authorization", "Bearer "+token)
		}
		resp, err := server.client.Do(req)
		if err != nil {
			return fmt.Errorf("error while fetching chunk for %q: %s", endPoint, err)
		}
		if resp.StatusCode == 404 && isFirstRequest {
			resp.Body.Close()
			return nil
		} else if resp.StatusCode != 200 {
			resp.Body.Close()
			return fmt.Errorf("acvp: HTTP error %d", resp.StatusCode)
		}
		isFirstRequest = false

		reply := reflect.New(replyType)
		err = parseReply(reply.Interface(), resp.Body)
		resp.Body.Close()
		if err != nil {
			return err
		}

		data := reply.Elem().FieldByName("Data")
		for i := 0; i < data.Len(); i++ {
			outputSlice.Set(reflect.Append(outputSlice, data.Index(i)))
		}

		if uint64(outputSlice.Len()) == reply.Elem().FieldByName("TotalCount").Uint() ||
			reply.Elem().FieldByName("Links").FieldByName("Next").String() == "" {
			break
		}

		nextURL = server.prefix + endPoint + fmt.Sprintf("?offset=%d", outputSlice.Len())
		if len(conditionParams) > 0 {
			nextURL += "&" + conditionParams
		}
	}

	return nil
}

// https://pages.nist.gov/ACVP/draft-fussell-acvp-spec.html#rfc.section.11.8.3.1
type Vendor struct {
	URL         string    `json:"url,omitempty"`
	Name        string    `json:"name,omitempty"`
	ParentURL   string    `json:"parentUrl,omitempty"`
	Website     string    `json:"website,omitempty"`
	Emails      []string  `json:"emails,omitempty"`
	ContactsURL string    `json:"contactsUrl,omitempty"`
	Addresses   []Address `json:"addresses,omitempty"`
}

// https://pages.nist.gov/ACVP/draft-fussell-acvp-spec.html#rfc.section.11.9
type Address struct {
	URL        string `json:"url,omitempty"`
	Street1    string `json:"street1,omitempty"`
	Street2    string `json:"street2,omitempty"`
	Street3    string `json:"street3,omitempty"`
	Locality   string `json:"locality,omitempty"`
	Region     string `json:"region,omitempty"`
	Country    string `json:"country,omitempty"`
	PostalCode string `json:"postalCode,omitempty"`
}

// https://pages.nist.gov/ACVP/draft-fussell-acvp-spec.html#rfc.section.11.10
type Person struct {
	URL          string   `json:"url,omitempty"`
	FullName     string   `json:"fullName,omitempty"`
	VendorURL    string   `json:"vendorUrl,omitempty"`
	Emails       []string `json:"emails,omitempty"`
	PhoneNumbers []struct {
		Number string `json:"number,omitempty"`
		Type   string `json:"type,omitempty"`
	} `json:"phoneNumbers,omitempty"`
}

// https://pages.nist.gov/ACVP/draft-fussell-acvp-spec.html#rfc.section.11.11
type Module struct {
	URL         string   `json:"url,omitempty"`
	Name        string   `json:"name,omitempty"`
	Version     string   `json:"version,omitempty"`
	Type        string   `json:"type,omitempty"`
	Website     string   `json:"website,omitempty"`
	VendorURL   string   `json:"vendorUrl,omitempty"`
	AddressURL  string   `json:"addressUrl,omitempty"`
	ContactURLs []string `json:"contactUrls,omitempty"`
	Description string   `json:"description,omitempty"`
}

type RequestStatus struct {
	URL         string `json:"url,omitempty"`
	Status      string `json:"status,omitempty"`
	Message     string `json:"message,omitempty"`
	ApprovedURL string `json:"approvedUrl,omitempty"`
}

type OperationalEnvironment struct {
	URL            string       `json:"url,omitempty"`
	Name           string       `json:"name,omitempty"`
	DependencyUrls []string     `json:"dependencyUrls,omitempty"`
	Dependencies   []Dependency `json:"dependencies,omitempty"`
}

type Dependency map[string]any

type Algorithm map[string]any

type TestSession struct {
	URL           string           `json:"url,omitempty"`
	ACVPVersion   string           `json:"acvpVersion,omitempty"`
	Created       string           `json:"createdOn,omitempty"`
	Expires       string           `json:"expiresOn,omitempty"`
	VectorSetURLs []string         `json:"vectorSetUrls,omitempty"`
	AccessToken   string           `json:"accessToken,omitempty"`
	Algorithms    []map[string]any `json:"algorithms,omitempty"`
	EncryptAtRest bool             `json:"encryptAtRest,omitempty"`
	IsSample      bool             `json:"isSample,omitempty"`
	Publishable   bool             `json:"publishable,omitempty"`
	Passed        bool             `json:"passed,omitempty"`
}

type Vectors struct {
	Retry    uint64 `json:"retry,omitempty"`
	ID       uint64 `json:"vsId"`
	Algo     string `json:"algorithm,omitempty"`
	Revision string `json:"revision,omitempty"`
}

type LargeUploadRequest struct {
	Size uint64 `json:"submissionSize,omitempty"`
	URL  string `json:"vectorSetUrl,omitempty"`
}

type LargeUploadResponse struct {
	URL         string `json:"url"`
	AccessToken string `json:"accessToken"`
}

type SessionResults struct {
	Passed  bool `json:"passed"`
	Results []struct {
		URL    string `json:"vectorSetUrl,omitempty"`
		Status string `json:"status"`
	} `json:"results"`
}
