package auth

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/google/uuid"
	"go.b0esche.cloud/backend/internal/database"
	"golang.org/x/crypto/argon2"
	"golang.org/x/crypto/bcrypt"
)

const (
	ChallengeLength = 32
	RPID            = "b0esche.cloud"
	RPName          = "b0esche Cloud"
	Origin          = "https://b0esche.cloud"

	// Argon2id parameters (OWASP recommendations)
	Argon2Time    = 2         // iterations
	Argon2Memory  = 19 * 1024 // 19 MB
	Argon2Threads = 1
	Argon2KeyLen  = 32
)

type Service struct {
	db *database.DB
}

func NewService(db *database.DB) *Service {
	return &Service{db: db}
}

// StartRegistrationChallenge creates a challenge for passkey registration
func (s *Service) StartRegistrationChallenge(ctx context.Context, userID uuid.UUID) (string, error) {
	challenge := make([]byte, ChallengeLength)
	if _, err := rand.Read(challenge); err != nil {
		return "", fmt.Errorf("failed to generate challenge: %w", err)
	}

	challengeStr := base64.StdEncoding.EncodeToString(challenge)

	// Store challenge in database
	if err := s.db.CreateAuthChallenge(ctx, userID, challenge, "registration"); err != nil {
		return "", fmt.Errorf("failed to store challenge: %w", err)
	}

	return challengeStr, nil
}

// StartAuthenticationChallenge creates a challenge for passkey authentication
func (s *Service) StartAuthenticationChallenge(ctx context.Context, username string) (string, []string, error) {
	// Get user by username
	user, err := s.db.GetUserByUsername(ctx, username)
	if err != nil {
		return "", nil, fmt.Errorf("user not found: %w", err)
	}

	challenge := make([]byte, ChallengeLength)
	if _, err := rand.Read(challenge); err != nil {
		return "", nil, fmt.Errorf("failed to generate challenge: %w", err)
	}

	challengeStr := base64.StdEncoding.EncodeToString(challenge)

	// Store challenge in database
	if err := s.db.CreateAuthChallenge(ctx, user.ID, challenge, "authentication"); err != nil {
		return "", nil, fmt.Errorf("failed to store challenge: %w", err)
	}

	// Get user's credentials
	credentials, err := s.db.GetUserCredentials(ctx, user.ID)
	if err != nil {
		return "", nil, fmt.Errorf("failed to get credentials: %w", err)
	}

	// Return credential IDs (base64 encoded for transport)
	var credentialIDs []string
	for _, cred := range credentials {
		credentialIDs = append(credentialIDs, base64.StdEncoding.EncodeToString(cred.CredentialID))
	}

	return challengeStr, credentialIDs, nil
}

// VerifyRegistrationResponse verifies the attestation response from the client
func (s *Service) VerifyRegistrationResponse(
	ctx context.Context,
	userID uuid.UUID,
	challengeB64 string,
	credentialIDBase64 string,
	publicKeyBase64 string,
	clientDataJSON string,
	attestationObjectBase64 string,
) (*database.Credential, error) {
	// Decode inputs
	challenge, err := base64.StdEncoding.DecodeString(challengeB64)
	if err != nil {
		return nil, fmt.Errorf("invalid challenge encoding: %w", err)
	}

	credentialID, err := base64.StdEncoding.DecodeString(credentialIDBase64)
	if err != nil {
		return nil, fmt.Errorf("invalid credential ID encoding: %w", err)
	}

	publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
	if err != nil {
		return nil, fmt.Errorf("invalid public key encoding: %w", err)
	}

	_, err = base64.StdEncoding.DecodeString(attestationObjectBase64)
	if err != nil {
		return nil, fmt.Errorf("invalid attestation object encoding: %w", err)
	}

	// Verify challenge exists and belongs to this user
	if err := s.verifyChallenge(ctx, userID, challenge, "registration"); err != nil {
		return nil, fmt.Errorf("challenge verification failed: %w", err)
	}

	// In production, you would parse and verify the attestation object here
	// For now, we'll just verify the client data matches
	var clientData struct {
		Type      string `json:"type"`
		Challenge string `json:"challenge"`
		Origin    string `json:"origin"`
	}

	if err := json.Unmarshal([]byte(clientDataJSON), &clientData); err != nil {
		return nil, fmt.Errorf("invalid client data JSON: %w", err)
	}

	// Verify challenge in client data
	clientDataChallenge, err := base64.StdEncoding.DecodeString(clientData.Challenge)
	if err != nil {
		return nil, fmt.Errorf("invalid challenge in client data: %w", err)
	}

	// Verify challenge matches (we skip the hash verification since it's not needed for API validation)
	// clientDataHash := sha256.Sum256([]byte(clientDataJSON))

	// Verify challenge matches
	if !byteArraysEqual(clientDataChallenge, challenge) {
		return nil, fmt.Errorf("challenge mismatch")
	}

	// Verify origin
	if clientData.Origin != Origin {
		return nil, fmt.Errorf("origin mismatch: expected %s, got %s", Origin, clientData.Origin)
	}

	// Verify type
	if clientData.Type != "webauthn.create" {
		return nil, fmt.Errorf("invalid client data type: %s", clientData.Type)
	}

	// Store credential in database
	credential := &database.Credential{
		ID:                  base64.StdEncoding.EncodeToString(credentialID),
		UserID:              userID,
		CredentialPublicKey: publicKeyBytes,
		CredentialID:        credentialID,
		SignCount:           0,
	}

	if err := s.db.CreateCredential(ctx, credential); err != nil {
		return nil, fmt.Errorf("failed to store credential: %w", err)
	}

	// Mark challenge as used
	if err := s.db.MarkChallengeUsed(ctx, challenge); err != nil {
		return nil, fmt.Errorf("failed to mark challenge as used: %w", err)
	}

	return credential, nil
}

// VerifyAuthenticationResponse verifies the assertion response from the client
func (s *Service) VerifyAuthenticationResponse(
	ctx context.Context,
	username string,
	challengeB64 string,
	credentialIDBase64 string,
	authenticatorData string,
	clientDataJSON string,
	signatureBase64 string,
) (*database.User, error) {
	// Get user by username
	user, err := s.db.GetUserByUsername(ctx, username)
	if err != nil {
		return nil, fmt.Errorf("user not found: %w", err)
	}

	// Decode challenge
	challenge, err := base64.StdEncoding.DecodeString(challengeB64)
	if err != nil {
		return nil, fmt.Errorf("invalid challenge encoding: %w", err)
	}

	// Verify challenge
	if err := s.verifyChallenge(ctx, user.ID, challenge, "authentication"); err != nil {
		return nil, fmt.Errorf("challenge verification failed: %w", err)
	}

	// Decode credential ID
	credentialID, err := base64.StdEncoding.DecodeString(credentialIDBase64)
	if err != nil {
		return nil, fmt.Errorf("invalid credential ID encoding: %w", err)
	}

	// Get credential from database
	credential, err := s.db.GetCredentialByID(ctx, credentialID)
	if err != nil {
		return nil, fmt.Errorf("credential not found: %w", err)
	}

	// Verify credential belongs to user
	if credential.UserID != user.ID {
		return nil, fmt.Errorf("credential does not belong to user")
	}

	// Parse and verify client data
	var clientData struct {
		Type      string `json:"type"`
		Challenge string `json:"challenge"`
		Origin    string `json:"origin"`
	}

	if err := json.Unmarshal([]byte(clientDataJSON), &clientData); err != nil {
		return nil, fmt.Errorf("invalid client data JSON: %w", err)
	}

	// Verify challenge matches
	clientDataChallenge, err := base64.StdEncoding.DecodeString(clientData.Challenge)
	if err != nil {
		return nil, fmt.Errorf("invalid challenge in client data: %w", err)
	}

	if !byteArraysEqual(clientDataChallenge, challenge) {
		return nil, fmt.Errorf("challenge mismatch")
	}

	// Verify origin
	if clientData.Origin != Origin {
		return nil, fmt.Errorf("origin mismatch: expected %s, got %s", Origin, clientData.Origin)
	}

	// Verify type
	if clientData.Type != "webauthn.get" {
		return nil, fmt.Errorf("invalid client data type: %s", clientData.Type)
	}

	// In production, you would verify the signature here using the public key
	// For now, we'll assume the signature is valid if we got this far

	// Mark challenge as used
	if err := s.db.MarkChallengeUsed(ctx, challenge); err != nil {
		return nil, fmt.Errorf("failed to mark challenge as used: %w", err)
	}

	// Update credential last used time
	if err := s.db.UpdateCredentialLastUsed(ctx, credential.ID); err != nil {
		return nil, fmt.Errorf("failed to update credential last used: %w", err)
	}

	// Update user last login
	if err := s.db.UpdateUserLastLogin(ctx, user.ID); err != nil {
		return nil, fmt.Errorf("failed to update user last login: %w", err)
	}

	return user, nil
}

func (s *Service) verifyChallenge(ctx context.Context, userID uuid.UUID, challenge []byte, challengeType string) error {
	return s.db.VerifyAuthChallenge(ctx, userID, challenge, challengeType)
}

func byteArraysEqual(a, b []byte) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

// HashPassword hashes a password using Argon2id (quantum-resistant)
// Format: $argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>
func (s *Service) HashPassword(password string) (string, error) {
	// Generate 16-byte random salt
	salt := make([]byte, 16)
	if _, err := rand.Read(salt); err != nil {
		return "", fmt.Errorf("failed to generate salt: %w", err)
	}

	// Hash with Argon2id
	hash := argon2.IDKey([]byte(password), salt, Argon2Time, Argon2Memory, Argon2Threads, Argon2KeyLen)

	// Encode in PHC string format
	b64Salt := base64.RawStdEncoding.EncodeToString(salt)
	b64Hash := base64.RawStdEncoding.EncodeToString(hash)

	return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
		Argon2Memory, Argon2Time, Argon2Threads, b64Salt, b64Hash), nil
}

// VerifyPassword checks if a password matches its hash
// Supports both Argon2id (new) and bcrypt (legacy) for backward compatibility
func (s *Service) VerifyPassword(passwordHash string, password string) bool {
	// Detect hash format
	if strings.HasPrefix(passwordHash, "$argon2id$") {
		return s.verifyArgon2(passwordHash, password)
	} else if strings.HasPrefix(passwordHash, "$2") {
		// Legacy bcrypt hash
		err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password))
		return err == nil
	}
	return false
}

func (s *Service) verifyArgon2(encodedHash string, password string) bool {
	// Parse PHC format: $argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>
	parts := strings.Split(encodedHash, "$")
	if len(parts) != 6 {
		return false
	}

	var memory, time uint32
	var threads uint8
	_, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads)
	if err != nil {
		return false
	}

	salt, err := base64.RawStdEncoding.DecodeString(parts[4])
	if err != nil {
		return false
	}

	hash, err := base64.RawStdEncoding.DecodeString(parts[5])
	if err != nil {
		return false
	}

	// Compute hash with same parameters
	computedHash := argon2.IDKey([]byte(password), salt, time, memory, threads, uint32(len(hash)))

	// Constant-time comparison
	if len(hash) != len(computedHash) {
		return false
	}
	var diff byte
	for i := 0; i < len(hash); i++ {
		diff |= hash[i] ^ computedHash[i]
	}
	return diff == 0
}

// VerifyPasswordLogin verifies username and password credentials
func (s *Service) VerifyPasswordLogin(ctx context.Context, username, password string) (*database.User, error) {
	user, err := s.db.GetUserByUsername(ctx, username)
	if err != nil {
		return nil, fmt.Errorf("user not found: %w", err)
	}

	if user.PasswordHash == nil || *user.PasswordHash == "" {
		return nil, fmt.Errorf("user does not have a password set")
	}

	if !s.VerifyPassword(*user.PasswordHash, password) {
		return nil, fmt.Errorf("invalid password")
	}

	// Update last login
	if err := s.db.UpdateUserLastLogin(ctx, user.ID); err != nil {
		return nil, fmt.Errorf("failed to update user last login: %w", err)
	}

	return user, nil
}
