package service

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"strings"
	"time"

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

	"wucher/internal/domain/auth"
)

const (
	webAuthnCeremonyRegistration = "registration"
	webAuthnCeremonyLogin        = "login"
)

var (
	ErrWebAuthnNotConfigured         = errors.New("webauthn not configured")
	ErrWebAuthnChallengeInvalid      = errors.New("invalid or expired webauthn challenge")
	ErrWebAuthnPayloadInvalid        = errors.New("invalid webauthn payload")
	ErrWebAuthnCredentialNotFound    = errors.New("webauthn credential not found")
	ErrWebAuthnCredentialExists      = errors.New("webauthn credential already exists")
	ErrWebAuthnCredentialUnavailable = errors.New("webauthn credential is not configured for this account")
	ErrWebAuthnVerificationFailed    = errors.New("webauthn verification failed")
)

type webAuthnChallengeEnvelope struct {
	Ceremony string                  `json:"ceremony"`
	UserID   []byte                  `json:"user_id"`
	Session  webauthnlib.SessionData `json:"session"`
}

type webAuthnUser struct {
	id          []byte
	name        string
	displayName string
	credentials []webauthnlib.Credential
}

func (u webAuthnUser) WebAuthnID() []byte {
	return append([]byte(nil), u.id...)
}

func (u webAuthnUser) WebAuthnName() string {
	return u.name
}

func (u webAuthnUser) WebAuthnDisplayName() string {
	return u.displayName
}

func (u webAuthnUser) WebAuthnCredentials() []webauthnlib.Credential {
	return append([]webauthnlib.Credential(nil), u.credentials...)
}

func (s *AuthService) WebAuthnChallengeTTL() time.Duration {
	ttl := s.cfg.WebAuthnChallengeTTL
	if ttl <= 0 {
		return 5 * time.Minute
	}
	return ttl
}

func (s *AuthService) IsWebAuthnConfigured(ctx context.Context, userID []byte) (bool, error) {
	records, err := s.repo.ListUserWebAuthnCredentials(ctx, userID)
	if err != nil {
		return false, err
	}
	return len(records) > 0, nil
}

func (s *AuthService) BeginWebAuthnRegistration(ctx context.Context, userID []byte) (*protocol.CredentialCreation, string, error) {
	if len(userID) != 16 {
		return nil, "", ErrInvalidToken
	}
	client, err := s.webAuthnClient()
	if err != nil {
		return nil, "", err
	}

	user, err := s.repo.GetUserByID(ctx, userID)
	if err != nil {
		return nil, "", err
	}
	if user == nil {
		return nil, "", ErrInvalidToken
	}

	creds, err := s.loadWebAuthnCredentials(ctx, userID)
	if err != nil {
		return nil, "", err
	}
	waUser := buildWebAuthnUser(user, creds)

	opts := []webauthnlib.RegistrationOption{
		webauthnlib.WithConveyancePreference(protocol.PreferNoAttestation),
		webauthnlib.WithResidentKeyRequirement(protocol.ResidentKeyRequirementPreferred),
	}
	if len(creds) > 0 {
		opts = append(opts, webauthnlib.WithExclusions(webauthnlib.Credentials(creds).CredentialDescriptors()))
	}

	creation, session, err := client.BeginRegistration(waUser, opts...)
	if err != nil {
		return nil, "", err
	}
	challengeToken, err := s.storeWebAuthnChallenge(ctx, webAuthnCeremonyRegistration, userID, session)
	if err != nil {
		return nil, "", err
	}
	s.logServiceAudit(ctx, "auth.webauthn.register.begin", userID, true, "challenge_issued", nil)
	return creation, challengeToken, nil
}

func (s *AuthService) FinishWebAuthnRegistration(ctx context.Context, userID []byte, challengeToken string, credentialJSON []byte, name string) error {
	if len(userID) != 16 {
		return ErrInvalidToken
	}
	client, err := s.webAuthnClient()
	if err != nil {
		return err
	}
	session, err := s.consumeWebAuthnChallenge(ctx, challengeToken, webAuthnCeremonyRegistration)
	if err != nil {
		s.logServiceAudit(ctx, "auth.webauthn.register.finish", userID, false, "invalid_challenge", nil)
		return err
	}
	if !bytes.Equal(session.UserID, userID) {
		s.logServiceAudit(ctx, "auth.webauthn.register.finish", userID, false, "challenge_user_mismatch", nil)
		return ErrWebAuthnChallengeInvalid
	}

	user, err := s.repo.GetUserByID(ctx, userID)
	if err != nil {
		return err
	}
	if user == nil {
		return ErrInvalidToken
	}
	creds, err := s.loadWebAuthnCredentials(ctx, userID)
	if err != nil {
		return err
	}
	waUser := buildWebAuthnUser(user, creds)

	parsed, err := protocol.ParseCredentialCreationResponseBody(bytes.NewReader(credentialJSON))
	if err != nil {
		s.logServiceAudit(ctx, "auth.webauthn.register.finish", userID, false, "invalid_payload", nil)
		return ErrWebAuthnPayloadInvalid
	}

	credential, err := client.CreateCredential(waUser, session.Session, parsed)
	if err != nil {
		s.logServiceAudit(ctx, "auth.webauthn.register.finish", userID, false, "verification_failed", nil)
		return ErrWebAuthnVerificationFailed
	}

	for i := range creds {
		if bytes.Equal(creds[i].ID, credential.ID) {
			return ErrWebAuthnCredentialExists
		}
	}

	serialized, err := json.Marshal(credential)
	if err != nil {
		return err
	}

	rec := &auth.UserWebAuthnCredential{
		UserID:         userID,
		Name:           truncate(strings.TrimSpace(name), 100),
		CredentialID:   append([]byte(nil), credential.ID...),
		CredentialJSON: serialized,
	}
	if err := s.repo.CreateUserWebAuthnCredential(ctx, rec); err != nil {
		return err
	}
	s.logServiceAudit(ctx, "auth.webauthn.register.finish", userID, true, "credential_registered", nil)
	return nil
}

func (s *AuthService) BeginWebAuthnLogin(ctx context.Context, email string) (*protocol.CredentialAssertion, string, error) {
	client, err := s.webAuthnClient()
	if err != nil {
		return nil, "", err
	}
	email = strings.ToLower(strings.TrimSpace(email))
	user, err := s.repo.GetUserByEmail(ctx, email)
	if err != nil {
		return nil, "", err
	}
	if user == nil {
		return nil, "", ErrInvalidCredentials
	}
	if !user.IsActive {
		return nil, "", ErrInvalidCredentials
	}
	if user.EmailVerifiedAt == nil {
		return nil, "", ErrEmailNotVerified
	}

	creds, err := s.loadWebAuthnCredentials(ctx, user.ID)
	if err != nil {
		return nil, "", err
	}
	if len(creds) == 0 {
		return nil, "", ErrWebAuthnCredentialUnavailable
	}

	waUser := buildWebAuthnUser(user, creds)
	assertion, session, err := client.BeginLogin(waUser, webauthnlib.WithUserVerification(protocol.VerificationRequired))
	if err != nil {
		return nil, "", err
	}
	challengeToken, err := s.storeWebAuthnChallenge(ctx, webAuthnCeremonyLogin, user.ID, session)
	if err != nil {
		return nil, "", err
	}
	s.logServiceAudit(ctx, "auth.webauthn.login.begin", user.ID, true, "challenge_issued", nil)
	return assertion, challengeToken, nil
}

func (s *AuthService) FinishWebAuthnLogin(ctx context.Context, challengeToken string, credentialJSON []byte) (*auth.User, error) {
	client, err := s.webAuthnClient()
	if err != nil {
		return nil, err
	}
	session, err := s.consumeWebAuthnChallenge(ctx, challengeToken, webAuthnCeremonyLogin)
	if err != nil {
		s.logServiceAudit(ctx, "auth.webauthn.login.finish", nil, false, "invalid_challenge", nil)
		return nil, err
	}
	if len(session.UserID) != 16 {
		return nil, ErrWebAuthnChallengeInvalid
	}

	user, err := s.repo.GetUserByID(ctx, session.UserID)
	if err != nil {
		return nil, err
	}
	if user == nil {
		return nil, ErrInvalidCredentials
	}

	creds, err := s.loadWebAuthnCredentials(ctx, session.UserID)
	if err != nil {
		return nil, err
	}
	if len(creds) == 0 {
		return nil, ErrWebAuthnCredentialUnavailable
	}
	waUser := buildWebAuthnUser(user, creds)

	parsed, err := protocol.ParseCredentialRequestResponseBody(bytes.NewReader(credentialJSON))
	if err != nil {
		s.logServiceAudit(ctx, "auth.webauthn.login.finish", session.UserID, false, "invalid_payload", nil)
		return nil, ErrWebAuthnPayloadInvalid
	}
	credential, err := client.ValidateLogin(waUser, session.Session, parsed)
	if err != nil {
		s.logServiceAudit(ctx, "auth.webauthn.login.finish", session.UserID, false, "verification_failed", nil)
		return nil, ErrWebAuthnVerificationFailed
	}

	serialized, err := json.Marshal(credential)
	if err != nil {
		return nil, err
	}
	if err := s.repo.UpdateUserWebAuthnCredential(ctx, session.UserID, credential.ID, serialized, time.Now().UTC()); err != nil {
		return nil, err
	}

	s.logServiceAudit(ctx, "auth.webauthn.login.finish", session.UserID, true, "login_success", nil)
	return user, nil
}

func (s *AuthService) webAuthnClient() (*webauthnlib.WebAuthn, error) {
	rpID := strings.TrimSpace(s.cfg.WebAuthnRPID)
	rpDisplayName := strings.TrimSpace(s.cfg.WebAuthnRPDisplayName)
	rpOrigins := make([]string, 0, len(s.cfg.WebAuthnRPOrigins))
	for i := range s.cfg.WebAuthnRPOrigins {
		origin := strings.TrimSpace(s.cfg.WebAuthnRPOrigins[i])
		if origin == "" {
			continue
		}
		rpOrigins = append(rpOrigins, origin)
	}

	if rpID == "" || len(rpOrigins) == 0 {
		return nil, ErrWebAuthnNotConfigured
	}
	if rpDisplayName == "" {
		rpDisplayName = "Wucher"
	}

	ttl := s.WebAuthnChallengeTTL()
	return webauthnlib.New(&webauthnlib.Config{
		RPID:                  rpID,
		RPDisplayName:         rpDisplayName,
		RPOrigins:             rpOrigins,
		AttestationPreference: protocol.PreferNoAttestation,
		AuthenticatorSelection: protocol.AuthenticatorSelection{
			ResidentKey:        protocol.ResidentKeyRequirementPreferred,
			RequireResidentKey: protocol.ResidentKeyNotRequired(),
			UserVerification:   protocol.VerificationRequired,
		},
		Timeouts: webauthnlib.TimeoutsConfig{
			Login: webauthnlib.TimeoutConfig{
				Enforce:    true,
				Timeout:    ttl,
				TimeoutUVD: ttl,
			},
			Registration: webauthnlib.TimeoutConfig{
				Enforce:    true,
				Timeout:    ttl,
				TimeoutUVD: ttl,
			},
		},
	})
}

func (s *AuthService) loadWebAuthnCredentials(ctx context.Context, userID []byte) ([]webauthnlib.Credential, error) {
	records, err := s.repo.ListUserWebAuthnCredentials(ctx, userID)
	if err != nil {
		return nil, err
	}
	out := make([]webauthnlib.Credential, 0, len(records))
	for i := range records {
		var credential webauthnlib.Credential
		if err := json.Unmarshal(records[i].CredentialJSON, &credential); err != nil {
			return nil, err
		}
		out = append(out, credential)
	}
	return out, nil
}

func buildWebAuthnUser(user *auth.User, credentials []webauthnlib.Credential) webAuthnUser {
	name := strings.TrimSpace(user.Email)
	displayName := strings.TrimSpace(strings.TrimSpace(user.FirstName) + " " + strings.TrimSpace(user.LastName))
	if displayName == "" {
		displayName = name
	}
	return webAuthnUser{
		id:          append([]byte(nil), user.ID...),
		name:        name,
		displayName: displayName,
		credentials: credentials,
	}
}

func (s *AuthService) storeWebAuthnChallenge(ctx context.Context, ceremony string, userID []byte, session *webauthnlib.SessionData) (string, error) {
	if s.tokens == nil {
		return "", ErrWebAuthnChallengeInvalid
	}
	if session == nil {
		return "", ErrWebAuthnChallengeInvalid
	}

	token, err := randomToken(32)
	if err != nil {
		return "", err
	}
	envelope := webAuthnChallengeEnvelope{
		Ceremony: ceremony,
		UserID:   append([]byte(nil), userID...),
		Session:  *session,
	}
	payload, err := json.Marshal(envelope)
	if err != nil {
		return "", err
	}
	if err := s.tokens.Set(ctx, webAuthnChallengeKey(token), payload, s.WebAuthnChallengeTTL()); err != nil {
		return "", err
	}
	return token, nil
}

func (s *AuthService) consumeWebAuthnChallenge(ctx context.Context, token, ceremony string) (*webAuthnChallengeEnvelope, error) {
	token = strings.TrimSpace(token)
	if token == "" || s.tokens == nil {
		return nil, ErrWebAuthnChallengeInvalid
	}

	raw, err := s.tokens.Get(ctx, webAuthnChallengeKey(token))
	if err != nil || raw == nil {
		return nil, ErrWebAuthnChallengeInvalid
	}
	_ = s.tokens.Delete(ctx, webAuthnChallengeKey(token))

	var envelope webAuthnChallengeEnvelope
	if err := json.Unmarshal(raw, &envelope); err != nil {
		return nil, ErrWebAuthnChallengeInvalid
	}
	if strings.TrimSpace(envelope.Ceremony) != ceremony {
		return nil, ErrWebAuthnChallengeInvalid
	}
	if !envelope.Session.Expires.IsZero() && envelope.Session.Expires.Before(time.Now().UTC()) {
		return nil, ErrWebAuthnChallengeInvalid
	}
	return &envelope, nil
}

func webAuthnChallengeKey(token string) string {
	return "auth:webauthn:challenge:" + token
}
