package integrationservice

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"io"
	"net/url"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"
)

const (
	mailboxStateVersion          = 1
	mailboxStateAAD              = "nexuscrm.integration.connected-mail.state.v1"
	maxMailboxStatePayloadBytes  = 8 * 1024
	maxMailboxStateEnvelopeBytes = 12 * 1024
)

var errMailboxStateInvalid = errors.New("integration service: connected-mail state is invalid")

// mailboxState binds the provider callback to the Runtime authorization that
// admitted the browser request. The sealed value is carried by the provider;
// Integration does not depend on a same-site browser cookie at callback time.
type mailboxState struct {
	Version               uint32    `json:"version"`
	OAuthState            string    `json:"oauthState"`
	AuthorizedSubjectID   string    `json:"authorizedSubjectId"`
	ReauthorizationHandle string    `json:"reauthorizationHandle"`
	ConnectorKey          string    `json:"connectorKey"`
	CallbackURI           string    `json:"callbackUri"`
	ScopeDigest           string    `json:"scopeDigest"`
	ReturnTarget          string    `json:"returnTarget"`
	ExpiresAt             time.Time `json:"expiresAt"`
}

type mailboxStateCodec struct {
	aead   cipher.AEAD
	random io.Reader
	now    func() time.Time
}

func newMailboxStateCodec(masterKey []byte) (*mailboxStateCodec, error) {
	if len(masterKey) != 32 {
		return nil, errMailboxStateInvalid
	}
	derivation := hmac.New(sha256.New, masterKey)
	_, _ = derivation.Write([]byte(mailboxStateAAD))
	key := derivation.Sum(nil)
	defer clearConnectedMailBytes(key)
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, errMailboxStateInvalid
	}
	aead, err := cipher.NewGCM(block)
	if err != nil {
		return nil, errMailboxStateInvalid
	}
	return &mailboxStateCodec{aead: aead, random: rand.Reader, now: time.Now}, nil
}

func (codec *mailboxStateCodec) seal(state mailboxState) (string, error) {
	if codec == nil || codec.aead == nil || codec.random == nil || codec.now == nil {
		return "", errMailboxStateInvalid
	}
	state.Version = mailboxStateVersion
	if !validMailboxState(state, codec.now().UTC()) {
		return "", errMailboxStateInvalid
	}
	raw, err := json.Marshal(state)
	if err != nil || len(raw) > maxMailboxStatePayloadBytes {
		return "", errMailboxStateInvalid
	}
	defer clearConnectedMailBytes(raw)
	nonce := make([]byte, codec.aead.NonceSize())
	if _, err := io.ReadFull(codec.random, nonce); err != nil {
		return "", errMailboxStateInvalid
	}
	sealed := codec.aead.Seal(nil, nonce, raw, []byte(mailboxStateAAD))
	envelope := append(nonce, sealed...)
	encoded := "v1." + base64.RawURLEncoding.EncodeToString(envelope)
	if len(encoded) > maxMailboxStateEnvelopeBytes {
		return "", errMailboxStateInvalid
	}
	return encoded, nil
}

func (codec *mailboxStateCodec) open(encoded string) (mailboxState, error) {
	if codec == nil || codec.aead == nil || codec.now == nil ||
		len(encoded) > maxMailboxStateEnvelopeBytes || !strings.HasPrefix(encoded, "v1.") {
		return mailboxState{}, errMailboxStateInvalid
	}
	envelope, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(encoded, "v1."))
	if err != nil || len(envelope) <= codec.aead.NonceSize() {
		return mailboxState{}, errMailboxStateInvalid
	}
	nonce := envelope[:codec.aead.NonceSize()]
	plaintext, err := codec.aead.Open(
		nil,
		nonce,
		envelope[codec.aead.NonceSize():],
		[]byte(mailboxStateAAD),
	)
	if err != nil || len(plaintext) > maxMailboxStatePayloadBytes {
		clearConnectedMailBytes(plaintext)
		return mailboxState{}, errMailboxStateInvalid
	}
	defer clearConnectedMailBytes(plaintext)
	decoder := json.NewDecoder(bytes.NewReader(plaintext))
	decoder.DisallowUnknownFields()
	var state mailboxState
	if decoder.Decode(&state) != nil || decoder.Decode(&struct{}{}) != io.EOF ||
		!validMailboxState(state, codec.now().UTC()) {
		return mailboxState{}, errMailboxStateInvalid
	}
	return state, nil
}

func validMailboxState(state mailboxState, now time.Time) bool {
	if state.Version != mailboxStateVersion ||
		!boundedMailboxStateValue(state.OAuthState, 2048) ||
		!boundedMailboxStateValue(state.AuthorizedSubjectID, 255) ||
		!boundedMailboxStateValue(state.ReauthorizationHandle, 1024) ||
		!boundedMailboxStateValue(state.ConnectorKey, 255) ||
		!boundedMailboxStateValue(state.CallbackURI, 2048) ||
		!validMailboxScopeDigest(state.ScopeDigest) ||
		!validMailboxReturnTarget(state.ReturnTarget) ||
		!state.ExpiresAt.After(now) || state.ExpiresAt.After(now.Add(15*time.Minute)) {
		return false
	}
	callback, err := url.Parse(state.CallbackURI)
	return err == nil && callback.Host != "" && callback.User == nil &&
		callback.RawQuery == "" && callback.Fragment == "" && callback.Opaque == "" &&
		(callback.Scheme == "https" ||
			(callback.Scheme == "http" && loopbackMailboxHost(callback.Hostname())))
}

func boundedMailboxStateValue(value string, maximum int) bool {
	return value != "" && len(value) <= maximum && utf8.ValidString(value) &&
		value == strings.TrimSpace(value) && strings.IndexFunc(value, unicode.IsControl) < 0
}

func validMailboxScopeDigest(value string) bool {
	if len(value) != len("sha256:")+sha256HexLength || !strings.HasPrefix(value, "sha256:") {
		return false
	}
	decoded, err := hex.DecodeString(strings.TrimPrefix(value, "sha256:"))
	return err == nil && len(decoded) == sha256HexLength/2
}

const sha256HexLength = 64

func validMailboxReturnTarget(value string) bool {
	if value == "" || len(value) > 2048 || !strings.HasPrefix(value, "/") ||
		strings.HasPrefix(value, "//") || strings.ContainsAny(value, "\\\x00\r\n") {
		return false
	}
	parsed, err := url.Parse(value)
	return err == nil && parsed.Scheme == "" && parsed.Host == "" && parsed.User == nil &&
		parsed.Opaque == "" && !strings.HasPrefix(parsed.Path, "//") &&
		!strings.Contains(parsed.Path, "\\")
}

func loopbackMailboxHost(host string) bool {
	switch strings.ToLower(strings.TrimSpace(host)) {
	case "localhost", "127.0.0.1", "::1":
		return true
	default:
		return false
	}
}
