package models

import (
	"database/sql/driver"
	"fmt"

	"encoding/json"
	"github.com/go-webauthn/webauthn/webauthn"
	"github.com/gofrs/uuid"
	"github.com/supabase/auth/internal/crypto"
	"github.com/supabase/auth/internal/storage"
	"time"
)

type Challenge struct {
	ID                  uuid.UUID            `json:"challenge_id" db:"id"`
	FactorID            uuid.UUID            `json:"factor_id" db:"factor_id"`
	CreatedAt           time.Time            `json:"created_at" db:"created_at"`
	VerifiedAt          *time.Time           `json:"verified_at,omitempty" db:"verified_at"`
	IPAddress           string               `json:"ip_address" db:"ip_address"`
	Factor              *Factor              `json:"factor,omitempty" belongs_to:"factor"`
	OtpCode             string               `json:"otp_code,omitempty" db:"otp_code"`
	WebAuthnSessionData *WebAuthnSessionData `json:"web_authn_session_data,omitempty" db:"web_authn_session_data"`
}

type WebAuthnSessionData struct {
	*webauthn.SessionData
}

func (s *WebAuthnSessionData) Scan(value interface{}) error {
	if value == nil {
		s.SessionData = nil
		return nil
	}

	// Handle byte and string as a precaution, in postgres driver, json/jsonb should be returned as []byte
	var data []byte
	switch v := value.(type) {
	case []byte:
		data = v
	case string:
		data = []byte(v)
	default:
		panic(fmt.Sprintf("unsupported type for web_authn_session_data: %T", value))
	}

	if len(data) == 0 {
		s.SessionData = nil
		return nil
	}
	if s.SessionData == nil {
		s.SessionData = &webauthn.SessionData{}
	}
	return json.Unmarshal(data, s.SessionData)

}

func (s *WebAuthnSessionData) Value() (driver.Value, error) {
	if s == nil || s.SessionData == nil {
		return nil, nil
	}
	return json.Marshal(s.SessionData)
}

func (ws *WebAuthnSessionData) ToChallenge(factorID uuid.UUID, ipAddress string) *Challenge {
	id := uuid.Must(uuid.NewV4())
	return &Challenge{
		ID:        id,
		FactorID:  factorID,
		IPAddress: ipAddress,
		WebAuthnSessionData: &WebAuthnSessionData{
			ws.SessionData,
		},
	}

}

func (Challenge) TableName() string {
	tableName := "mfa_challenges"
	return tableName
}

// Update the verification timestamp
func (c *Challenge) Verify(tx *storage.Connection) error {
	now := time.Now()
	c.VerifiedAt = &now
	return tx.UpdateOnly(c, "verified_at")
}

func (c *Challenge) HasExpired(expiryDuration float64) bool {
	return time.Now().After(c.GetExpiryTime(expiryDuration))
}

func (c *Challenge) GetExpiryTime(expiryDuration float64) time.Time {
	return c.CreatedAt.Add(time.Second * time.Duration(expiryDuration))
}

func (c *Challenge) SetOtpCode(otpCode string, encrypt bool, encryptionKeyID, encryptionKey string) error {
	c.OtpCode = otpCode
	if encrypt {
		es, err := crypto.NewEncryptedString(c.ID.String(), []byte(otpCode), encryptionKeyID, encryptionKey)
		if err != nil {
			return err
		}

		c.OtpCode = es.String()
	}
	return nil

}

func (c *Challenge) GetOtpCode(decryptionKeys map[string]string, encrypt bool, encryptionKeyID string) (string, bool, error) {
	if es := crypto.ParseEncryptedString(c.OtpCode); es != nil {
		bytes, err := es.Decrypt(c.ID.String(), decryptionKeys)
		if err != nil {
			return "", false, err
		}

		return string(bytes), encrypt && es.ShouldReEncrypt(encryptionKeyID), nil
	}

	return c.OtpCode, encrypt, nil

}
