package oidcsession

import (
	"crypto/rand"
	"encoding/base64"
	"errors"
	"net/url"
	"strings"
	"time"

	"github.com/google/uuid"
)

const LoginTransactionCookieName = "easyai_gateway_oidc_login"

var loginTransactionAAD = []byte("easyai-gateway/oidc-login-transaction/v1")

type LoginTransaction struct {
	State        string    `json:"state"`
	Nonce        string    `json:"nonce"`
	PKCEVerifier string    `json:"pkceVerifier"`
	ReturnTo     string    `json:"returnTo"`
	ContextType  string    `json:"contextType"`
	TenantHint   string    `json:"tenantHint,omitempty"`
	CreatedAt    time.Time `json:"createdAt"`
}

func NewLoginTransaction(returnTo string, now time.Time) (LoginTransaction, error) {
	return NewLoginTransactionWithContext(returnTo, "tenant", "", now)
}

func NewLoginTransactionWithTenantHint(returnTo, tenantHint string, now time.Time) (LoginTransaction, error) {
	return NewLoginTransactionWithContext(returnTo, "tenant", tenantHint, now)
}

func NewLoginTransactionWithContext(
	returnTo, contextType, tenantHint string,
	now time.Time,
) (LoginTransaction, error) {
	if !ValidReturnTo(returnTo) {
		return LoginTransaction{}, errors.New("returnTo must be a same-origin relative path")
	}
	contextType = strings.TrimSpace(contextType)
	tenantHint = strings.TrimSpace(tenantHint)
	if contextType != "" && contextType != "platform" && contextType != "tenant" {
		return LoginTransaction{}, errors.New("contextType must be empty, platform or tenant")
	}
	if contextType != "tenant" && tenantHint != "" {
		return LoginTransaction{}, errors.New("only tenant context can bind a tenant hint")
	}
	if tenantHint != "" && uuid.Validate(tenantHint) != nil {
		return LoginTransaction{}, errors.New("tenantHint must be a UUID")
	}
	state, err := randomBase64URL(32)
	if err != nil {
		return LoginTransaction{}, err
	}
	nonce, err := randomBase64URL(32)
	if err != nil {
		return LoginTransaction{}, err
	}
	verifier, err := randomBase64URL(32)
	if err != nil {
		return LoginTransaction{}, err
	}
	return LoginTransaction{
		State: state, Nonce: nonce, PKCEVerifier: verifier,
		ReturnTo: returnTo, ContextType: contextType,
		TenantHint: tenantHint, CreatedAt: now.UTC(),
	}, nil
}

func (c *Cipher) EncodeLoginTransaction(transaction LoginTransaction) (string, error) {
	payload, err := c.SealJSON(transaction, loginTransactionAAD)
	if err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(payload), nil
}

func (c *Cipher) DecodeLoginTransaction(encoded string, now time.Time) (LoginTransaction, error) {
	payload, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(encoded))
	if err != nil {
		return LoginTransaction{}, errors.New("OIDC login transaction is invalid")
	}
	var transaction LoginTransaction
	if err := c.OpenJSON(payload, loginTransactionAAD, &transaction); err != nil {
		return LoginTransaction{}, err
	}
	if transaction.State == "" || transaction.Nonce == "" || transaction.PKCEVerifier == "" || !ValidReturnTo(transaction.ReturnTo) ||
		transaction.ContextType != "" && transaction.ContextType != "platform" && transaction.ContextType != "tenant" ||
		transaction.ContextType != "tenant" && transaction.TenantHint != "" ||
		transaction.TenantHint != "" && uuid.Validate(transaction.TenantHint) != nil ||
		transaction.CreatedAt.IsZero() || now.Before(transaction.CreatedAt.Add(-time.Minute)) || !now.Before(transaction.CreatedAt.Add(10*time.Minute)) {
		return LoginTransaction{}, errors.New("OIDC login transaction has expired or is invalid")
	}
	return transaction, nil
}

func ValidReturnTo(value string) bool {
	value = strings.TrimSpace(value)
	if value == "" || !strings.HasPrefix(value, "/") || strings.HasPrefix(value, "//") || strings.Contains(value, "\\") {
		return false
	}
	parsed, err := url.Parse(value)
	return err == nil && !parsed.IsAbs() && parsed.Host == ""
}

func randomBase64URL(size int) (string, error) {
	value := make([]byte, size)
	if _, err := rand.Read(value); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(value), nil
}
