package pb

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"time"

	"github.com/golang/protobuf/ptypes"
	"github.com/hashicorp/go-secure-stdlib/parseutil"
	"github.com/hashicorp/vault/sdk/helper/errutil"
	"github.com/hashicorp/vault/sdk/helper/wrapping"
	"github.com/hashicorp/vault/sdk/logical"
)

const (
	ErrTypeUnknown uint32 = iota
	ErrTypeUserError
	ErrTypeInternalError
	ErrTypeCodedError
	ErrTypeStatusBadRequest
	ErrTypeUnsupportedOperation
	ErrTypeUnsupportedPath
	ErrTypeInvalidRequest
	ErrTypePermissionDenied
	ErrTypeMultiAuthzPending
	ErrTypeUnrecoverable
)

func ProtoErrToErr(e *ProtoError) error {
	if e == nil {
		return nil
	}

	var err error
	switch e.ErrType {
	case ErrTypeUnknown:
		err = errors.New(e.ErrMsg)
	case ErrTypeUserError:
		err = errutil.UserError{Err: e.ErrMsg}
	case ErrTypeInternalError:
		err = errutil.InternalError{Err: e.ErrMsg}
	case ErrTypeCodedError:
		err = logical.CodedError(int(e.ErrCode), e.ErrMsg)
	case ErrTypeStatusBadRequest:
		err = &logical.StatusBadRequest{Err: e.ErrMsg}
	case ErrTypeUnsupportedOperation:
		err = logical.ErrUnsupportedOperation
	case ErrTypeUnsupportedPath:
		err = logical.ErrUnsupportedPath
	case ErrTypeInvalidRequest:
		err = logical.ErrInvalidRequest
	case ErrTypePermissionDenied:
		err = logical.ErrPermissionDenied
	case ErrTypeMultiAuthzPending:
		err = logical.ErrMultiAuthzPending
	case ErrTypeUnrecoverable:
		err = logical.ErrUnrecoverable
	}

	return err
}

func ErrToProtoErr(e error) *ProtoError {
	if e == nil {
		return nil
	}
	pbErr := &ProtoError{
		ErrMsg:  e.Error(),
		ErrType: ErrTypeUnknown,
	}

	switch e.(type) {
	case errutil.UserError:
		pbErr.ErrType = ErrTypeUserError
	case errutil.InternalError:
		pbErr.ErrType = ErrTypeInternalError
	case logical.HTTPCodedError:
		pbErr.ErrType = ErrTypeCodedError
		pbErr.ErrCode = int64(e.(logical.HTTPCodedError).Code())
	case *logical.StatusBadRequest:
		pbErr.ErrType = ErrTypeStatusBadRequest
	}

	switch {
	case e == logical.ErrUnsupportedOperation:
		pbErr.ErrType = ErrTypeUnsupportedOperation
	case e == logical.ErrUnsupportedPath:
		pbErr.ErrType = ErrTypeUnsupportedPath
	case e == logical.ErrInvalidRequest:
		pbErr.ErrType = ErrTypeInvalidRequest
	case e == logical.ErrPermissionDenied:
		pbErr.ErrType = ErrTypePermissionDenied
	case e == logical.ErrMultiAuthzPending:
		pbErr.ErrType = ErrTypeMultiAuthzPending
	case e == logical.ErrUnrecoverable:
		pbErr.ErrType = ErrTypeUnrecoverable
	}

	return pbErr
}

func ErrToString(e error) string {
	if e == nil {
		return ""
	}

	return e.Error()
}

func LogicalStorageEntryToProtoStorageEntry(e *logical.StorageEntry) *StorageEntry {
	if e == nil {
		return nil
	}

	return &StorageEntry{
		Key:      e.Key,
		Value:    e.Value,
		SealWrap: e.SealWrap,
	}
}

func ProtoStorageEntryToLogicalStorageEntry(e *StorageEntry) *logical.StorageEntry {
	if e == nil {
		return nil
	}

	return &logical.StorageEntry{
		Key:      e.Key,
		Value:    e.Value,
		SealWrap: e.SealWrap,
	}
}

func ProtoLeaseOptionsToLogicalLeaseOptions(l *LeaseOptions) (logical.LeaseOptions, error) {
	if l == nil {
		return logical.LeaseOptions{}, nil
	}

	t, err := ptypes.Timestamp(l.IssueTime)
	return logical.LeaseOptions{
		TTL:       time.Duration(l.TTL),
		Renewable: l.Renewable,
		Increment: time.Duration(l.Increment),
		IssueTime: t,
		MaxTTL:    time.Duration(l.MaxTTL),
	}, err
}

func LogicalLeaseOptionsToProtoLeaseOptions(l logical.LeaseOptions) (*LeaseOptions, error) {
	t, err := ptypes.TimestampProto(l.IssueTime)
	if err != nil {
		return nil, err
	}

	return &LeaseOptions{
		TTL:       int64(l.TTL),
		Renewable: l.Renewable,
		Increment: int64(l.Increment),
		IssueTime: t,
		MaxTTL:    int64(l.MaxTTL),
	}, err
}

func ProtoSecretToLogicalSecret(s *Secret) (*logical.Secret, error) {
	if s == nil {
		return nil, nil
	}

	data := map[string]interface{}{}
	err := json.Unmarshal([]byte(s.InternalData), &data)
	if err != nil {
		return nil, err
	}

	lease, err := ProtoLeaseOptionsToLogicalLeaseOptions(s.LeaseOptions)
	if err != nil {
		return nil, err
	}

	return &logical.Secret{
		LeaseOptions: lease,
		InternalData: data,
		LeaseID:      s.LeaseID,
	}, nil
}

func LogicalSecretToProtoSecret(s *logical.Secret) (*Secret, error) {
	if s == nil {
		return nil, nil
	}

	buf, err := json.Marshal(s.InternalData)
	if err != nil {
		return nil, err
	}

	lease, err := LogicalLeaseOptionsToProtoLeaseOptions(s.LeaseOptions)
	if err != nil {
		return nil, err
	}

	return &Secret{
		LeaseOptions: lease,
		InternalData: string(buf[:]),
		LeaseID:      s.LeaseID,
	}, err
}

func LogicalRequestToProtoRequest(r *logical.Request) (*Request, error) {
	if r == nil {
		return nil, nil
	}

	buf, err := json.Marshal(r.Data)
	if err != nil {
		return nil, err
	}

	secret, err := LogicalSecretToProtoSecret(r.Secret)
	if err != nil {
		return nil, err
	}

	auth, err := LogicalAuthToProtoAuth(r.Auth)
	if err != nil {
		return nil, err
	}

	headers := map[string]*Header{}
	for k, v := range r.Headers {
		headers[k] = &Header{Header: v}
	}

	return &Request{
		ID:                       r.ID,
		ReplicationCluster:       r.ReplicationCluster,
		Operation:                string(r.Operation),
		Path:                     r.Path,
		Data:                     string(buf[:]),
		Secret:                   secret,
		Auth:                     auth,
		Headers:                  headers,
		ClientToken:              r.ClientToken,
		ClientTokenAccessor:      r.ClientTokenAccessor,
		DisplayName:              r.DisplayName,
		MountPoint:               r.MountPoint,
		MountType:                r.MountType,
		MountAccessor:            r.MountAccessor,
		WrapInfo:                 LogicalRequestWrapInfoToProtoRequestWrapInfo(r.WrapInfo),
		ClientTokenRemainingUses: int64(r.ClientTokenRemainingUses),
		Connection:               LogicalConnectionToProtoConnection(r.Connection),
		EntityID:                 r.EntityID,
		PolicyOverride:           r.PolicyOverride,
		Unauthenticated:          r.Unauthenticated,
	}, nil
}

func ProtoRequestToLogicalRequest(r *Request) (*logical.Request, error) {
	if r == nil {
		return nil, nil
	}

	data := map[string]interface{}{}
	err := json.Unmarshal([]byte(r.Data), &data)
	if err != nil {
		return nil, err
	}

	secret, err := ProtoSecretToLogicalSecret(r.Secret)
	if err != nil {
		return nil, err
	}

	auth, err := ProtoAuthToLogicalAuth(r.Auth)
	if err != nil {
		return nil, err
	}

	var headers map[string][]string
	if len(r.Headers) > 0 {
		headers = make(map[string][]string, len(r.Headers))
		for k, v := range r.Headers {
			headers[k] = v.Header
		}
	}

	connection, err := ProtoConnectionToLogicalConnection(r.Connection)
	if err != nil {
		return nil, err
	}

	return &logical.Request{
		ID:                       r.ID,
		ReplicationCluster:       r.ReplicationCluster,
		Operation:                logical.Operation(r.Operation),
		Path:                     r.Path,
		Data:                     data,
		Secret:                   secret,
		Auth:                     auth,
		Headers:                  headers,
		ClientToken:              r.ClientToken,
		ClientTokenAccessor:      r.ClientTokenAccessor,
		DisplayName:              r.DisplayName,
		MountPoint:               r.MountPoint,
		MountType:                r.MountType,
		MountAccessor:            r.MountAccessor,
		WrapInfo:                 ProtoRequestWrapInfoToLogicalRequestWrapInfo(r.WrapInfo),
		ClientTokenRemainingUses: int(r.ClientTokenRemainingUses),
		Connection:               connection,
		EntityID:                 r.EntityID,
		PolicyOverride:           r.PolicyOverride,
		Unauthenticated:          r.Unauthenticated,
	}, nil
}

func LogicalConnectionToProtoConnection(c *logical.Connection) *Connection {
	if c == nil {
		return nil
	}

	return &Connection{
		RemoteAddr:      c.RemoteAddr,
		ConnectionState: TLSConnectionStateToProtoConnectionState(c.ConnState),
	}
}

func ProtoConnectionToLogicalConnection(c *Connection) (*logical.Connection, error) {
	if c == nil {
		return nil, nil
	}

	cs, err := ProtoConnectionStateToTLSConnectionState(c.ConnectionState)
	if err != nil {
		return nil, err
	}

	return &logical.Connection{
		RemoteAddr: c.RemoteAddr,
		ConnState:  cs,
	}, nil
}

func LogicalRequestWrapInfoToProtoRequestWrapInfo(i *logical.RequestWrapInfo) *RequestWrapInfo {
	if i == nil {
		return nil
	}

	return &RequestWrapInfo{
		TTL:      int64(i.TTL),
		Format:   i.Format,
		SealWrap: i.SealWrap,
	}
}

func ProtoRequestWrapInfoToLogicalRequestWrapInfo(i *RequestWrapInfo) *logical.RequestWrapInfo {
	if i == nil {
		return nil
	}

	return &logical.RequestWrapInfo{
		TTL:      time.Duration(i.TTL),
		Format:   i.Format,
		SealWrap: i.SealWrap,
	}
}

func ProtoResponseToLogicalResponse(r *Response) (*logical.Response, error) {
	if r == nil {
		return nil, nil
	}

	secret, err := ProtoSecretToLogicalSecret(r.Secret)
	if err != nil {
		return nil, err
	}

	auth, err := ProtoAuthToLogicalAuth(r.Auth)
	if err != nil {
		return nil, err
	}

	data := map[string]interface{}{}
	err = json.Unmarshal([]byte(r.Data), &data)
	if err != nil {
		return nil, err
	}

	wrapInfo, err := ProtoResponseWrapInfoToLogicalResponseWrapInfo(r.WrapInfo)
	if err != nil {
		return nil, err
	}

	var headers map[string][]string
	if len(r.Headers) > 0 {
		headers = make(map[string][]string, len(r.Headers))
		for k, v := range r.Headers {
			headers[k] = v.Header
		}
	}

	return &logical.Response{
		Secret:   secret,
		Auth:     auth,
		Data:     data,
		Redirect: r.Redirect,
		Warnings: r.Warnings,
		WrapInfo: wrapInfo,
		Headers:  headers,
	}, nil
}

func ProtoResponseWrapInfoToLogicalResponseWrapInfo(i *ResponseWrapInfo) (*wrapping.ResponseWrapInfo, error) {
	if i == nil {
		return nil, nil
	}

	t, err := ptypes.Timestamp(i.CreationTime)
	if err != nil {
		return nil, err
	}

	return &wrapping.ResponseWrapInfo{
		TTL:             time.Duration(i.TTL),
		Token:           i.Token,
		Accessor:        i.Accessor,
		CreationTime:    t,
		WrappedAccessor: i.WrappedAccessor,
		WrappedEntityID: i.WrappedEntityID,
		Format:          i.Format,
		CreationPath:    i.CreationPath,
		SealWrap:        i.SealWrap,
	}, nil
}

func LogicalResponseWrapInfoToProtoResponseWrapInfo(i *wrapping.ResponseWrapInfo) (*ResponseWrapInfo, error) {
	if i == nil {
		return nil, nil
	}

	t, err := ptypes.TimestampProto(i.CreationTime)
	if err != nil {
		return nil, err
	}

	return &ResponseWrapInfo{
		TTL:             int64(i.TTL),
		Token:           i.Token,
		Accessor:        i.Accessor,
		CreationTime:    t,
		WrappedAccessor: i.WrappedAccessor,
		WrappedEntityID: i.WrappedEntityID,
		Format:          i.Format,
		CreationPath:    i.CreationPath,
		SealWrap:        i.SealWrap,
	}, nil
}

func LogicalResponseToProtoResponse(r *logical.Response) (*Response, error) {
	if r == nil {
		return nil, nil
	}

	secret, err := LogicalSecretToProtoSecret(r.Secret)
	if err != nil {
		return nil, err
	}

	auth, err := LogicalAuthToProtoAuth(r.Auth)
	if err != nil {
		return nil, err
	}

	buf, err := json.Marshal(r.Data)
	if err != nil {
		return nil, err
	}

	wrapInfo, err := LogicalResponseWrapInfoToProtoResponseWrapInfo(r.WrapInfo)
	if err != nil {
		return nil, err
	}

	headers := map[string]*Header{}
	for k, v := range r.Headers {
		headers[k] = &Header{Header: v}
	}

	return &Response{
		Secret:   secret,
		Auth:     auth,
		Data:     string(buf[:]),
		Redirect: r.Redirect,
		Warnings: r.Warnings,
		WrapInfo: wrapInfo,
		Headers:  headers,
	}, nil
}

func LogicalAuthToProtoAuth(a *logical.Auth) (*Auth, error) {
	if a == nil {
		return nil, nil
	}

	buf, err := json.Marshal(a.InternalData)
	if err != nil {
		return nil, err
	}

	lo, err := LogicalLeaseOptionsToProtoLeaseOptions(a.LeaseOptions)
	if err != nil {
		return nil, err
	}

	boundCIDRs := make([]string, len(a.BoundCIDRs))
	for i, cidr := range a.BoundCIDRs {
		boundCIDRs[i] = cidr.String()
	}

	return &Auth{
		LeaseOptions:     lo,
		TokenType:        uint32(a.TokenType),
		InternalData:     string(buf[:]),
		DisplayName:      a.DisplayName,
		Policies:         a.Policies,
		TokenPolicies:    a.TokenPolicies,
		IdentityPolicies: a.IdentityPolicies,
		NoDefaultPolicy:  a.NoDefaultPolicy,
		Metadata:         a.Metadata,
		ClientToken:      a.ClientToken,
		Accessor:         a.Accessor,
		Period:           int64(a.Period),
		NumUses:          int64(a.NumUses),
		EntityID:         a.EntityID,
		Alias:            a.Alias,
		GroupAliases:     a.GroupAliases,
		BoundCIDRs:       boundCIDRs,
		ExplicitMaxTTL:   int64(a.ExplicitMaxTTL),
	}, nil
}

func ProtoAuthToLogicalAuth(a *Auth) (*logical.Auth, error) {
	if a == nil {
		return nil, nil
	}

	data := map[string]interface{}{}
	err := json.Unmarshal([]byte(a.InternalData), &data)
	if err != nil {
		return nil, err
	}

	lo, err := ProtoLeaseOptionsToLogicalLeaseOptions(a.LeaseOptions)
	if err != nil {
		return nil, err
	}

	boundCIDRs, err := parseutil.ParseAddrs(a.BoundCIDRs)
	if err != nil {
		return nil, err
	}
	if len(boundCIDRs) == 0 {
		// On inbound auths, if auth.BoundCIDRs is empty, it will be nil.
		// Let's match that behavior outbound.
		boundCIDRs = nil
	}

	return &logical.Auth{
		LeaseOptions:     lo,
		TokenType:        logical.TokenType(a.TokenType),
		InternalData:     data,
		DisplayName:      a.DisplayName,
		Policies:         a.Policies,
		TokenPolicies:    a.TokenPolicies,
		IdentityPolicies: a.IdentityPolicies,
		NoDefaultPolicy:  a.NoDefaultPolicy,
		Metadata:         a.Metadata,
		ClientToken:      a.ClientToken,
		Accessor:         a.Accessor,
		Period:           time.Duration(a.Period),
		NumUses:          int(a.NumUses),
		EntityID:         a.EntityID,
		Alias:            a.Alias,
		GroupAliases:     a.GroupAliases,
		BoundCIDRs:       boundCIDRs,
		ExplicitMaxTTL:   time.Duration(a.ExplicitMaxTTL),
	}, nil
}

func LogicalTokenEntryToProtoTokenEntry(t *logical.TokenEntry) *TokenEntry {
	if t == nil {
		return nil
	}

	boundCIDRs := make([]string, len(t.BoundCIDRs))
	for i, cidr := range t.BoundCIDRs {
		boundCIDRs[i] = cidr.String()
	}

	return &TokenEntry{
		ID:                 t.ID,
		Accessor:           t.Accessor,
		Parent:             t.Parent,
		Policies:           t.Policies,
		InlinePolicy:       t.InlinePolicy,
		Path:               t.Path,
		Meta:               t.Meta,
		InternalMeta:       t.InternalMeta,
		DisplayName:        t.DisplayName,
		NumUses:            int64(t.NumUses),
		CreationTime:       t.CreationTime,
		TTL:                int64(t.TTL),
		ExplicitMaxTTL:     int64(t.ExplicitMaxTTL),
		Role:               t.Role,
		Period:             int64(t.Period),
		EntityID:           t.EntityID,
		NoIdentityPolicies: t.NoIdentityPolicies,
		BoundCIDRs:         boundCIDRs,
		NamespaceID:        t.NamespaceID,
		CubbyholeID:        t.CubbyholeID,
		Type:               uint32(t.Type),
	}
}

func ProtoTokenEntryToLogicalTokenEntry(t *TokenEntry) (*logical.TokenEntry, error) {
	if t == nil {
		return nil, nil
	}

	boundCIDRs, err := parseutil.ParseAddrs(t.BoundCIDRs)
	if err != nil {
		return nil, err
	}
	if len(boundCIDRs) == 0 {
		// On inbound auths, if auth.BoundCIDRs is empty, it will be nil.
		// Let's match that behavior outbound.
		boundCIDRs = nil
	}

	return &logical.TokenEntry{
		ID:                 t.ID,
		Accessor:           t.Accessor,
		Parent:             t.Parent,
		Policies:           t.Policies,
		InlinePolicy:       t.InlinePolicy,
		Path:               t.Path,
		Meta:               t.Meta,
		InternalMeta:       t.InternalMeta,
		DisplayName:        t.DisplayName,
		NumUses:            int(t.NumUses),
		CreationTime:       t.CreationTime,
		TTL:                time.Duration(t.TTL),
		ExplicitMaxTTL:     time.Duration(t.ExplicitMaxTTL),
		Role:               t.Role,
		Period:             time.Duration(t.Period),
		EntityID:           t.EntityID,
		NoIdentityPolicies: t.NoIdentityPolicies,
		BoundCIDRs:         boundCIDRs,
		NamespaceID:        t.NamespaceID,
		CubbyholeID:        t.CubbyholeID,
		Type:               logical.TokenType(t.Type),
	}, nil
}

func TLSConnectionStateToProtoConnectionState(connState *tls.ConnectionState) *ConnectionState {
	if connState == nil {
		return nil
	}

	var verifiedChains []*CertificateChain

	if lvc := len(connState.VerifiedChains); lvc > 0 {
		verifiedChains = make([]*CertificateChain, lvc)
		for i, vc := range connState.VerifiedChains {
			verifiedChains[i] = CertificateChainToProtoCertificateChain(vc)
		}
	}

	return &ConnectionState{
		Version:                     uint32(connState.Version),
		HandshakeComplete:           connState.HandshakeComplete,
		DidResume:                   connState.DidResume,
		CipherSuite:                 uint32(connState.CipherSuite),
		NegotiatedProtocol:          connState.NegotiatedProtocol,
		NegotiatedProtocolIsMutual:  connState.NegotiatedProtocolIsMutual,
		ServerName:                  connState.ServerName,
		PeerCertificates:            CertificateChainToProtoCertificateChain(connState.PeerCertificates),
		VerifiedChains:              verifiedChains,
		SignedCertificateTimestamps: connState.SignedCertificateTimestamps,
		OcspResponse:                connState.OCSPResponse,
		TlsUnique:                   connState.TLSUnique,
	}
}

func ProtoConnectionStateToTLSConnectionState(cs *ConnectionState) (*tls.ConnectionState, error) {
	if cs == nil {
		return nil, nil
	}

	var (
		err              error
		peerCertificates []*x509.Certificate
		verifiedChains   [][]*x509.Certificate
	)

	if peerCertificates, err = ProtoCertificateChainToCertificateChain(cs.PeerCertificates); err != nil {
		return nil, err
	}

	if lvc := len(cs.VerifiedChains); lvc > 0 {
		verifiedChains = make([][]*x509.Certificate, lvc)
		for i, vc := range cs.VerifiedChains {
			if verifiedChains[i], err = ProtoCertificateChainToCertificateChain(vc); err != nil {
				return nil, err
			}
		}
	}

	connState := &tls.ConnectionState{
		Version:                     uint16(cs.Version),
		HandshakeComplete:           cs.HandshakeComplete,
		DidResume:                   cs.DidResume,
		CipherSuite:                 uint16(cs.CipherSuite),
		NegotiatedProtocol:          cs.NegotiatedProtocol,
		NegotiatedProtocolIsMutual:  cs.NegotiatedProtocolIsMutual,
		ServerName:                  cs.ServerName,
		PeerCertificates:            peerCertificates,
		VerifiedChains:              verifiedChains,
		SignedCertificateTimestamps: cs.SignedCertificateTimestamps,
		OCSPResponse:                cs.OcspResponse,
		TLSUnique:                   cs.TlsUnique,
	}

	return connState, nil
}

func CertificateChainToProtoCertificateChain(chain []*x509.Certificate) *CertificateChain {
	if len(chain) == 0 {
		return nil
	}

	cc := &CertificateChain{Certificates: make([]*Certificate, len(chain))}

	for i, c := range chain {
		cc.Certificates[i] = X509CertificateToProtoCertificate(c)
	}

	return cc
}

func ProtoCertificateChainToCertificateChain(cc *CertificateChain) ([]*x509.Certificate, error) {
	if cc == nil || len(cc.Certificates) == 0 {
		return nil, nil
	}

	certs := make([]*x509.Certificate, len(cc.Certificates))

	for i, c := range cc.Certificates {
		var err error
		if certs[i], err = ProtoCertificateToX509Certificate(c); err != nil {
			return nil, err
		}
	}

	return certs, nil
}

func X509CertificateToProtoCertificate(cert *x509.Certificate) *Certificate {
	if cert == nil {
		return nil
	}

	return &Certificate{Asn1Data: cert.Raw}
}

func ProtoCertificateToX509Certificate(c *Certificate) (*x509.Certificate, error) {
	if c == nil {
		return nil, nil
	}

	return x509.ParseCertificate(c.Asn1Data)
}
