// File Path: monorepo/cloud/maplepress-backend/pkg/security/password/breachcheck.go
package password

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"

	"go.uber.org/zap"
)

var (
	// ErrPasswordBreached indicates the password has been found in known data breaches
	ErrPasswordBreached = fmt.Errorf("password has been found in data breaches")
)

// BreachChecker checks if passwords have been compromised in known data breaches
// using the Have I Been Pwned API's k-anonymity model
type BreachChecker interface {
	// CheckPassword checks if a password has been breached
	// Returns the number of times the password was found in breaches (0 = safe)
	CheckPassword(ctx context.Context, password string) (int, error)

	// IsPasswordBreached returns true if password has been found in breaches
	IsPasswordBreached(ctx context.Context, password string) (bool, error)
}

type breachChecker struct {
	httpClient *http.Client
	apiURL     string
	userAgent  string
	logger     *zap.Logger
}

// NewBreachChecker creates a new password breach checker
// CWE-521: Password breach checking using Have I Been Pwned API
// Uses k-anonymity model - only sends first 5 characters of SHA-1 hash
func NewBreachChecker(logger *zap.Logger) BreachChecker {
	return &breachChecker{
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
		},
		apiURL:    "https://api.pwnedpasswords.com/range/",
		userAgent: "MaplePress-Backend-Password-Checker",
		logger:    logger.Named("breach-checker"),
	}
}

// CheckPassword checks if a password has been breached using HIBP k-anonymity API
// Returns the number of times the password appears in breaches (0 = safe)
// CWE-521: This implements password breach checking without sending the full password
func (bc *breachChecker) CheckPassword(ctx context.Context, password string) (int, error) {
	// Step 1: SHA-1 hash the password
	hash := sha1.Sum([]byte(password))
	hashStr := strings.ToUpper(hex.EncodeToString(hash[:]))

	// Step 2: Take first 5 characters (k-anonymity prefix)
	prefix := hashStr[:5]
	suffix := hashStr[5:]

	// Step 3: Query HIBP API with prefix only
	url := bc.apiURL + prefix

	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		bc.logger.Error("failed to create HIBP request", zap.Error(err))
		return 0, fmt.Errorf("failed to create request: %w", err)
	}

	// Set User-Agent as required by HIBP API
	req.Header.Set("User-Agent", bc.userAgent)
	req.Header.Set("Add-Padding", "true") // Request padding for additional privacy

	bc.logger.Debug("checking password against HIBP",
		zap.String("prefix", prefix))

	resp, err := bc.httpClient.Do(req)
	if err != nil {
		bc.logger.Error("failed to query HIBP API", zap.Error(err))
		return 0, fmt.Errorf("failed to query breach database: %w", err)
	}
	if resp == nil {
		bc.logger.Error("received nil response from HIBP API")
		return 0, fmt.Errorf("received nil response from breach database")
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bc.logger.Error("HIBP API returned non-OK status",
			zap.Int("status", resp.StatusCode))
		return 0, fmt.Errorf("breach database returned status %d", resp.StatusCode)
	}

	// Step 4: Read response body
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		bc.logger.Error("failed to read HIBP response", zap.Error(err))
		return 0, fmt.Errorf("failed to read response: %w", err)
	}

	// Step 5: Parse response and look for our suffix
	// Response format: SUFFIX:COUNT\r\n for each hash
	lines := strings.Split(string(body), "\r\n")
	for _, line := range lines {
		if line == "" {
			continue
		}

		parts := strings.Split(line, ":")
		if len(parts) != 2 {
			continue
		}

		// Check if this is our hash
		if parts[0] == suffix {
			count, err := strconv.Atoi(parts[1])
			if err != nil {
				bc.logger.Warn("failed to parse breach count",
					zap.String("line", line),
					zap.Error(err))
				return 0, fmt.Errorf("failed to parse breach count: %w", err)
			}

			bc.logger.Warn("password found in data breaches",
				zap.Int("breach_count", count))
			return count, nil
		}
	}

	// Password not found in breaches
	bc.logger.Debug("password not found in breaches")
	return 0, nil
}

// IsPasswordBreached returns true if password has been found in data breaches
// This is a convenience wrapper around CheckPassword
func (bc *breachChecker) IsPasswordBreached(ctx context.Context, password string) (bool, error) {
	count, err := bc.CheckPassword(ctx, password)
	if err != nil {
		return false, err
	}
	return count > 0, nil
}
