package auth

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
)

// User is the subset of the users row the auth module needs.
type User struct {
	ID          int64
	UUID        string
	Email       string
	PwHash      string
	DpUUID      string
	Status      string // "active" | "banned"
	TOTPEnabled bool   // two-factor enabled → login requires a second TOTP step
}

// Sentinel store errors. Service maps these to API errors.
var (
	// ErrEmailTaken is returned by CreateUserWithTrial on a duplicate email.
	ErrEmailTaken = errors.New("auth: email already registered")
	// ErrNotFound is returned when a user lookup yields no row.
	ErrNotFound = errors.New("auth: user not found")
)

// UserStore is the persistence contract for the auth module. The MySQL
// implementation lives in this file; tests substitute an in-memory fake.
type UserStore interface {
	// CreateUserWithTrial atomically inserts a new user and a 7-day PRO trial
	// subscription (source='trial') in a single transaction. The email UNIQUE
	// constraint guarantees a single trial per address; a duplicate returns
	// ErrEmailTaken. trialDays controls the trial length.
	CreateUserWithTrial(ctx context.Context, email, pwHash string, trialDays int) (*User, error)
	// GetUserByEmail returns the user for login. ErrNotFound when absent.
	GetUserByEmail(ctx context.Context, email string) (*User, error)
}

// SQLStore is the MySQL-backed UserStore.
type SQLStore struct {
	db *sql.DB
}

// NewSQLStore builds a SQLStore.
func NewSQLStore(db *sql.DB) *SQLStore { return &SQLStore{db: db} }

// CreateUserWithTrial implements UserStore.
func (s *SQLStore) CreateUserWithTrial(ctx context.Context, email, pwHash string, trialDays int) (*User, error) {
	userUUID := uuid.NewString()
	dpUUID := uuid.NewString()

	tx, err := s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
	if err != nil {
		return nil, fmt.Errorf("auth: begin tx: %w", err)
	}
	committed := false
	defer func() {
		if !committed {
			_ = tx.Rollback()
		}
	}()

	now := time.Now().UTC()
	res, err := tx.ExecContext(ctx,
		`INSERT INTO users (uuid, email, pw_hash, dp_uuid, status, created_at)
		 VALUES (?, ?, ?, ?, 'active', ?)`,
		userUUID, email, pwHash, dpUUID, now)
	if err != nil {
		if isDuplicateKey(err) {
			return nil, ErrEmailTaken
		}
		return nil, fmt.Errorf("auth: insert user: %w", err)
	}
	userID, err := res.LastInsertId()
	if err != nil {
		return nil, fmt.Errorf("auth: user last id: %w", err)
	}

	// Resolve the PRO plan id and insert the trial subscription.
	var proID int64
	if err := tx.QueryRowContext(ctx, `SELECT id FROM plans WHERE code='pro'`).Scan(&proID); err != nil {
		return nil, fmt.Errorf("auth: lookup pro plan: %w", err)
	}
	expires := now.AddDate(0, 0, trialDays)
	if _, err := tx.ExecContext(ctx,
		`INSERT INTO subscriptions (user_id, plan_id, expires_at, source, created_at)
		 VALUES (?, ?, ?, 'trial', ?)`,
		userID, proID, expires, now); err != nil {
		return nil, fmt.Errorf("auth: insert trial subscription: %w", err)
	}

	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("auth: commit: %w", err)
	}
	committed = true

	return &User{
		ID:     userID,
		UUID:   userUUID,
		Email:  email,
		PwHash: pwHash,
		DpUUID: dpUUID,
		Status: "active",
	}, nil
}

// GetUserByEmail implements UserStore.
func (s *SQLStore) GetUserByEmail(ctx context.Context, email string) (*User, error) {
	var u User
	err := s.db.QueryRowContext(ctx,
		`SELECT id, uuid, email, pw_hash, dp_uuid, status, totp_enabled FROM users WHERE email = ?`,
		email).Scan(&u.ID, &u.UUID, &u.Email, &u.PwHash, &u.DpUUID, &u.Status, &u.TOTPEnabled)
	if err == sql.ErrNoRows {
		return nil, ErrNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("auth: get user by email: %w", err)
	}
	return &u, nil
}

// isDuplicateKey reports whether err is a MySQL duplicate-key (1062) error.
func isDuplicateKey(err error) bool {
	if err == nil {
		return false
	}
	msg := err.Error()
	return strings.Contains(msg, "Duplicate entry") || strings.Contains(msg, "1062")
}
