package auth

import (
	"encoding/json"
	"fmt"
	"strings"

	"git.autistici.org/id/usermetadb"

	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
)

// Request to authenticate a user. It supports multiple methods for
// authentication including challenge-response 2FA.
type Request struct {
	Service          string
	Username         string
	Password         []byte
	OTP              string
	WebAuthnSession  *webauthn.SessionData
	WebAuthnResponse *protocol.ParsedCredentialAssertionData
	DeviceInfo       *usermetadb.DeviceInfo
}

func (r *Request) EncodeToMap(m map[string]string, prefix string) {
	m[prefix+"service"] = r.Service
	m[prefix+"username"] = r.Username
	m[prefix+"password"] = string(r.Password)

	if r.OTP != "" {
		m[prefix+"otp"] = r.OTP
	}
	// WebAuthN are encoded as opaque JSON blobs because they're way too
	// complex to safely serialize using our silly protocol.
	if r.WebAuthnSession != nil {
		m[prefix+"webauthn_session"] = encodeJSONBlob(r.WebAuthnSession)
	}
	if r.WebAuthnResponse != nil {
		m[prefix+"webauthn_response"] = encodeJSONBlob(r.WebAuthnResponse)
	}
	if r.DeviceInfo != nil {
		r.DeviceInfo.EncodeToMap(m, prefix+"device.")
	}
}

func (r *Request) DecodeFromMap(m map[string]string, prefix string) {
	r.Service = m[prefix+"service"]
	r.Username = m[prefix+"username"]
	r.Password = []byte(m[prefix+"password"])
	r.OTP = m[prefix+"otp"]
	if s := m[prefix+"webauthn_session"]; s != "" {
		var sess webauthn.SessionData
		if err := decodeJSONBlob(s, &sess); err == nil {
			r.WebAuthnSession = &sess
		}
	}
	if s := m[prefix+"webauthn_response"]; s != "" {
		var resp protocol.ParsedCredentialAssertionData
		if err := decodeJSONBlob(s, &resp); err == nil {
			r.WebAuthnResponse = &resp
		}
	}
	r.DeviceInfo = usermetadb.DecodeDeviceInfoFromMap(m, prefix+"device.")
}

// UserInfo contains optional user information that may be useful to
// authentication endpoints.
type UserInfo struct {
	Email  string
	Shard  string
	Groups []string
}

func encodeStringList(m map[string]string, prefix string, l []string) {
	for i, elem := range l {
		m[fmt.Sprintf("%s.%d.", prefix, i)] = elem
	}
}

func decodeStringList(m map[string]string, prefix string) (out []string) {
	i := 0
	for {
		s, ok := m[fmt.Sprintf("%s.%d.", prefix, i)]
		if !ok {
			break
		}
		out = append(out, s)
		i++
	}
	return
}

func (u *UserInfo) EncodeToMap(m map[string]string, prefix string) {
	if u.Email != "" {
		m[prefix+"email"] = u.Email
	}
	if u.Shard != "" {
		m[prefix+"shard"] = u.Shard
	}
	encodeStringList(m, prefix+"group", u.Groups)
}

func decodeUserInfoFromMap(m map[string]string, prefix string) *UserInfo {
	u := UserInfo{
		Email:  m[prefix+"email"],
		Shard:  m[prefix+"shard"],
		Groups: decodeStringList(m, prefix+"group"),
	}
	if u.Email == "" && u.Shard == "" && len(u.Groups) == 0 {
		return nil
	}
	return &u
}

// Response to an authentication request.
type Response struct {
	Status          Status
	Mechanism       Mechanism
	AuthenticatorID string
	TFAMethods      []TFAMethod
	WebAuthnSession *webauthn.SessionData
	WebAuthnData    *protocol.CredentialAssertion
	UserInfo        *UserInfo
}

// Has2FAMethod checks for the presence of a two-factor authentication
// method in the Response.
func (r *Response) Has2FAMethod(needle TFAMethod) bool {
	for _, m := range r.TFAMethods {
		if m == needle {
			return true
		}
	}
	return false
}

func encodeTFAMethodList(m map[string]string, prefix string, l []TFAMethod) {
	if len(l) == 0 {
		return
	}
	tmp := make([]string, 0, len(l))
	for _, el := range l {
		tmp = append(tmp, string(el))
	}
	encodeStringList(m, prefix, tmp)
}

func decodeTFAMethodList(m map[string]string, prefix string) []TFAMethod {
	l := decodeStringList(m, prefix)
	if len(l) == 0 {
		return nil
	}
	out := make([]TFAMethod, 0, len(l))
	for _, el := range l {
		out = append(out, TFAMethod(el))
	}
	return out
}

func (r *Response) EncodeToMap(m map[string]string, prefix string) {
	m[prefix+"status"] = r.Status.String()
	if r.Mechanism != "" {
		m[prefix+"mechanism"] = string(r.Mechanism)
	}
	if r.AuthenticatorID != "" {
		m[prefix+"authenticator_id"] = r.AuthenticatorID
	}
	encodeTFAMethodList(m, prefix+"2fa_methods", r.TFAMethods)
	if r.WebAuthnSession != nil {
		m[prefix+"webauthn_session"] = encodeJSONBlob(r.WebAuthnSession)
	}
	if r.WebAuthnData != nil {
		m[prefix+"webauthn_data"] = encodeJSONBlob(r.WebAuthnData)
	}
	if r.UserInfo != nil {
		r.UserInfo.EncodeToMap(m, prefix+"user.")
	}
}

func (r *Response) DecodeFromMap(m map[string]string, prefix string) {
	r.Status = parseAuthStatus(m[prefix+"status"])
	r.TFAMethods = decodeTFAMethodList(m, prefix+"2fa_methods")
	r.Mechanism = Mechanism(m[prefix+"mechanism"])
	r.AuthenticatorID = m[prefix+"authenticator_id"]
	if s := m[prefix+"webauthn_session"]; s != "" {
		var sess webauthn.SessionData
		if err := decodeJSONBlob(s, &sess); err == nil {
			r.WebAuthnSession = &sess
		}
	}
	if s := m[prefix+"webauthn_data"]; s != "" {
		var data protocol.CredentialAssertion
		if err := decodeJSONBlob(s, &data); err == nil {
			r.WebAuthnData = &data
		}
	}
	r.UserInfo = decodeUserInfoFromMap(m, prefix+"user.")
}

// Mechanism is an enum for the possible authentication mechanisms.
type Mechanism string

// Possible methods that were used to achieve successful authentication.
const (
	MechanismPassword = "password"
	MechanismASP      = "asp"
	MechanismOTP      = "otp"
	MechanismWebAuthN = "webauthn"
)

func (m Mechanism) String() string { return string(m) }

// TFAMethod is a hint provided to the caller with the type of 2FA
// method that is available for authentication.
type TFAMethod string

// Known second-factor auth methods.
const (
	TFAMethodNone = ""
	TFAMethodOTP  = "otp"
	TFAMethodU2F  = "u2f"
)

// Status of an authentication request.
type Status int

// Possible response statuses.
const (
	StatusOK = iota
	StatusInsufficientCredentials
	StatusError
)

func (s Status) String() string {
	switch s {
	case StatusOK:
		return "ok"
	case StatusInsufficientCredentials:
		return "insufficient_credentials"
	case StatusError:
		return "error"
	default:
		return fmt.Sprintf("unknown(%d)", int(s))
	}
}

func parseAuthStatus(s string) Status {
	switch s {
	case "ok":
		return StatusOK
	case "insufficient_credentials":
		return StatusInsufficientCredentials
	default:
		return StatusError
	}
}

func (s Status) Error() string {
	return s.String()
}

// Miscellaneous serializers for external objects.
func encodeJSONBlob(obj interface{}) string {
	data, err := json.Marshal(obj)
	if err != nil {
		panic(err)
	}
	return string(data)
}

func decodeJSONBlob(s string, obj interface{}) error {
	return json.NewDecoder(strings.NewReader(s)).Decode(obj)
}