package jess

import (
	"errors"
	"fmt"
)

// Envelope holds configuration for jess to put data into a letter.
type Envelope struct { //nolint:maligned // TODO
	Version uint8
	Name    string
	Tools   []string

	// Secret keys and passwords
	Secrets []*Signet

	// Sender related signets
	// When closing: private keys for signatures
	// When opening: public keys for signatures
	Senders []*Signet

	// Recipient related signets
	// When closing: public keys for key exchange or key encapsulation
	// When opening: private keys for key exchange or key encapsulation
	Recipients []*Signet

	// For users, envelopes describe how a letter is closed.
	// Therefore Secrets and Senders always refer to private keys and Recipients to public keys in that context.
	// These distictions are important in order for the user to easily and confidently distinguish what is going to happen. Think of it as "human security".

	MinimumSecurityLevel int
	No                   string
	requirements         *Requirements

	// flag to signify if envelope is used for opening
	opening bool
}

// NewUnconfiguredEnvelope returns an unconfigured, but slightly initialized envelope.
func NewUnconfiguredEnvelope() *Envelope {
	e := &Envelope{
		Version:      1,
		requirements: NewRequirements(),
	}
	e.SerializeRequirements()
	return e
}

// Correspondence returns a new session configured with the envelope.
func (e *Envelope) Correspondence(trustStore TrustStore) (*Session, error) {
	return e.initCorrespondence(trustStore, false)
}

func (e *Envelope) initCorrespondence(trustStore TrustStore, verifying bool) (*Session, error) {
	err := e.LoadRequirements()
	if err != nil {
		return nil, err
	}

	if verifying {
		// prep sender signets only
		err = e.prepSignets(e.Senders, e.opening, trustStore)
	} else {
		// prep all signets
		err = e.PrepareSignets(trustStore)
	}
	if err != nil {
		return nil, err
	}

	return newSession(e)
}

// WireCorrespondence returns a new wire session (live communication) configured with the envelope.
func (e *Envelope) WireCorrespondence(trustStore TrustStore) (*Session, error) {
	s, err := e.Correspondence(trustStore)
	if err != nil {
		return nil, err
	}

	err = s.initWireSession()
	if err != nil {
		return nil, err
	}

	return s, nil
}

// Check returns whether the envelope is valid and can be used as is.
func (e *Envelope) Check(trustStore TrustStore) error {
	_, err := e.Correspondence(trustStore)
	return err
}

// NoRecipientAuth removes the requirement to authenticate the recipient.
func (e *Envelope) NoRecipientAuth() *Envelope {
	if e.requirements == nil {
		e.requirements = NewRequirements()
	}

	e.requirements.Remove(RecipientAuthentication)
	return e
}

// NoSenderAuth removes the requirement to authenticate the sender.
func (e *Envelope) NoSenderAuth() *Envelope {
	if e.requirements == nil {
		e.requirements = NewRequirements()
	}

	e.requirements.Remove(SenderAuthentication)
	e.SerializeRequirements()
	return e
}

// NoConfidentiality removes the requirement to provide confidentiality.
func (e *Envelope) NoConfidentiality() *Envelope {
	if e.requirements == nil {
		e.requirements = NewRequirements()
	}

	e.requirements.Remove(Confidentiality)
	e.SerializeRequirements()
	return e
}

// NoIntegrity removes the requirement to provide integrity.
func (e *Envelope) NoIntegrity() *Envelope {
	if e.requirements == nil {
		e.requirements = NewRequirements()
	}

	e.requirements.Remove(Integrity)
	e.SerializeRequirements()
	return e
}

// Unsafe removes all requirements.
func (e *Envelope) Unsafe() *Envelope {
	e.requirements = &Requirements{}
	e.SerializeRequirements()
	return e
}

// Requirements returns the required requirements.
func (e *Envelope) Requirements() *Requirements {
	return e.requirements
}

// SetRequirements sets new requirements.
func (e *Envelope) SetRequirements(requirements *Requirements) {
	e.requirements = requirements
}

// LoadRequirements loads the required requirements from the struct's exposed negated "No" specification.
func (e *Envelope) LoadRequirements() error {
	if e.requirements == nil {
		attrs, err := ParseRequirementsFromNoSpec(e.No)
		if err != nil {
			return nil
		}

		e.requirements = attrs
	}
	return nil
}

// SerializeRequirements saves the requirement requirements in the struct's exposed negated "No" specification.
func (e *Envelope) SerializeRequirements() {
	e.No = e.requirements.SerializeToNoSpec()
}

// LoopSecrets loops over all secrets of the given scheme.
func (e *Envelope) LoopSecrets(scheme string, fn func(*Signet) error) error {
	for _, signet := range e.Secrets {
		if len(scheme) == 0 || signet.Scheme == scheme {
			err := fn(signet)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

// LoopSenders loops over all sender signets of the given scheme.
func (e *Envelope) LoopSenders(scheme string, fn func(*Signet) error) error {
	for _, signet := range e.Senders {
		if len(scheme) == 0 || signet.Scheme == scheme {
			err := fn(signet)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

// LoopRecipients loops over all recipient signets of the given scheme.
func (e *Envelope) LoopRecipients(scheme string, fn func(*Signet) error) error {
	for _, signet := range e.Recipients {
		if len(scheme) == 0 || signet.Scheme == scheme {
			err := fn(signet)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

// PrepareSignets checks that all signets of the envelope are ready to use. It will fetch referenced signets and load the keys.
func (e *Envelope) PrepareSignets(storage TrustStore) error {
	err := e.prepSignets(e.Secrets, e.opening, storage)
	if err != nil {
		return err
	}

	err = e.prepSignets(e.Senders, e.opening, storage)
	if err != nil {
		return err
	}

	return e.prepSignets(e.Recipients, !e.opening, storage)
}

// prepSignets checks that all signets of the envelope are ready to use.
func (e *Envelope) prepSignets(signets []*Signet, recipients bool, storage TrustStore) error {
	for i, signet := range signets {
		// load from storage
		if len(signet.Key) == 0 {
			if signet.Scheme == SignetSchemePassword {
				err := fillPassword(signet, !recipients, storage, e.MinimumSecurityLevel)
				if err != nil {
					return fmt.Errorf(`failed to get password for "%s": %s`, signet.ID, err)
				}
				continue
			}

			// signet is referrer
			if len(signet.ID) == 0 {
				return errors.New("signets must have a scheme+key or an ID")
			}

			// check if we have a storage
			if storage == nil {
				return fmt.Errorf(`failed to get signet with ID "%s": no truststore provided`, signet.ID)
			}

			// get signet from trust store
			new, err := storage.GetSignet(signet.ID, recipients)
			if err != nil {
				return fmt.Errorf(`failed to get signet with ID "%s" from truststore: %s`, signet.ID, err)
			}

			// check for scheme mismatch
			if signet.Scheme != "" && signet.Scheme != new.Scheme {
				return fmt.Errorf(`failed to apply signet with ID "%s" from truststore: was expected to be of type %s, but is %s`, signet.ID, signet.Scheme, new.Scheme)
			}

			// apply signet back into envelope
			signet = new
			signets[i] = new
		}

		// unwrap protection
		if signet.Protection != nil {
			return errors.New("protected signets are not yet supported")
		}

		// load signet
		switch signet.Scheme {
		case SignetSchemeKey, SignetSchemePassword:
			// no loading needed
		default:
			err := signet.LoadKey()
			if err != nil {
				return err
			}
		}

	}

	return nil
}

func fillPassword(signet *Signet, createPassword bool, storage TrustStore, minSecurityLevel int) (err error) {
	if createPassword {
		if createPasswordCallback == nil {
			return nil // ignore
		}
	} else if getPasswordCallback == nil {
		return nil
	}

	// find reference
	if signet.Info == nil || signet.Info.Name == "" {
		// check trust store for name
		if len(signet.ID) > 0 && storage != nil {
			// get signet from trust store
			new, err := storage.GetSignet(signet.ID, false)
			if err == nil && new.Info != nil {
				if signet.Info == nil {
					signet.Info = new.Info
				} else {
					signet.Info.Name = new.Info.Name
				}
			}
		}
	}

	if createPassword {
		return createPasswordCallback(signet, minSecurityLevel)
	}
	return getPasswordCallback(signet)
}