package auth

import (
	"crypto/rand"
	"encoding/hex"
	"errors"
	"time"

	"golang.org/x/crypto/bcrypt"
	"github.com/ajaxray/geek-life/model"
	"github.com/ajaxray/geek-life/repository"
)

var (
	ErrInvalidCredentials = errors.New("invalid credentials")
	ErrUserExists         = errors.New("user already exists")
	ErrTenantExists       = errors.New("tenant already exists")
)

// AuthService handles authentication operations
type AuthService struct {
	userRepo    repository.UserRepository
	tenantRepo  repository.TenantRepository
	sessionRepo repository.SessionRepository
	sessionDuration int // in hours
}

// NewAuthService creates a new authentication service
func NewAuthService(userRepo repository.UserRepository, tenantRepo repository.TenantRepository, sessionRepo repository.SessionRepository, sessionDuration int) *AuthService {
	return &AuthService{
		userRepo:        userRepo,
		tenantRepo:      tenantRepo,
		sessionRepo:     sessionRepo,
		sessionDuration: sessionDuration,
	}
}

// RegisterTenant creates a new tenant with an admin user
func (s *AuthService) RegisterTenant(tenantName, username, email, password string) (*model.UserContext, string, error) {
	// Check if tenant already exists
	if _, err := s.tenantRepo.GetByName(tenantName); err == nil {
		return nil, "", ErrTenantExists
	}

	// Create tenant
	tenant, err := s.tenantRepo.Create(tenantName)
	if err != nil {
		return nil, "", err
	}

	// Check if user already exists in this tenant
	if _, err := s.userRepo.GetByUsername(tenant.ID, username); err == nil {
		return nil, "", ErrUserExists
	}
	if _, err := s.userRepo.GetByEmail(tenant.ID, email); err == nil {
		return nil, "", ErrUserExists
	}

	// Hash password
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return nil, "", err
	}

	// Create user
	user, err := s.userRepo.Create(tenant.ID, username, email, string(hashedPassword))
	if err != nil {
		return nil, "", err
	}

	// Create session
	token, err := s.generateToken()
	if err != nil {
		return nil, "", err
	}

	expiresAt := time.Now().Add(time.Duration(s.sessionDuration) * time.Hour).Unix()
	_, err = s.sessionRepo.Create(user.ID, token, expiresAt)
	if err != nil {
		return nil, "", err
	}

	ctx := &model.UserContext{
		User:   user,
		Tenant: tenant,
	}

	return ctx, token, nil
}

// RegisterUser creates a new user in an existing tenant
func (s *AuthService) RegisterUser(tenantName, username, email, password string) (*model.UserContext, string, error) {
	// Get tenant
	tenant, err := s.tenantRepo.GetByName(tenantName)
	if err != nil {
		return nil, "", err
	}

	// Check if user already exists in this tenant
	if _, err := s.userRepo.GetByUsername(tenant.ID, username); err == nil {
		return nil, "", ErrUserExists
	}
	if _, err := s.userRepo.GetByEmail(tenant.ID, email); err == nil {
		return nil, "", ErrUserExists
	}

	// Hash password
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return nil, "", err
	}

	// Create user
	user, err := s.userRepo.Create(tenant.ID, username, email, string(hashedPassword))
	if err != nil {
		return nil, "", err
	}

	// Create session
	token, err := s.generateToken()
	if err != nil {
		return nil, "", err
	}

	expiresAt := time.Now().Add(time.Duration(s.sessionDuration) * time.Hour).Unix()
	_, err = s.sessionRepo.Create(user.ID, token, expiresAt)
	if err != nil {
		return nil, "", err
	}

	ctx := &model.UserContext{
		User:   user,
		Tenant: tenant,
	}

	return ctx, token, nil
}

// Login authenticates a user and returns a session token
func (s *AuthService) Login(tenantName, username, password string) (*model.UserContext, string, error) {
	// Get tenant
	tenant, err := s.tenantRepo.GetByName(tenantName)
	if err != nil {
		return nil, "", ErrInvalidCredentials
	}

	// Get user by username or email
	var user *model.User
	user, err = s.userRepo.GetByUsername(tenant.ID, username)
	if err != nil {
		// Try by email
		user, err = s.userRepo.GetByEmail(tenant.ID, username)
		if err != nil {
			return nil, "", ErrInvalidCredentials
		}
	}

	// Verify password
	err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
	if err != nil {
		return nil, "", ErrInvalidCredentials
	}

	// Create session
	token, err := s.generateToken()
	if err != nil {
		return nil, "", err
	}

	expiresAt := time.Now().Add(time.Duration(s.sessionDuration) * time.Hour).Unix()
	_, err = s.sessionRepo.Create(user.ID, token, expiresAt)
	if err != nil {
		return nil, "", err
	}

	ctx := &model.UserContext{
		User:   user,
		Tenant: tenant,
	}

	return ctx, token, nil
}

// ValidateSession validates a session token and returns user context
func (s *AuthService) ValidateSession(token string) (*model.UserContext, error) {
	// Get session
	session, err := s.sessionRepo.GetByToken(token)
	if err != nil {
		return nil, err
	}

	// Get user
	user, err := s.userRepo.GetByID(session.UserID)
	if err != nil {
		return nil, err
	}

	// Get tenant
	tenant, err := s.tenantRepo.GetByID(user.TenantID)
	if err != nil {
		return nil, err
	}

	ctx := &model.UserContext{
		User:   user,
		Tenant: tenant,
	}

	return ctx, nil
}

// Logout invalidates a session
func (s *AuthService) Logout(token string) error {
	session, err := s.sessionRepo.GetByToken(token)
	if err != nil {
		return err
	}

	return s.sessionRepo.Delete(session)
}

// generateToken generates a random session token
func (s *AuthService) generateToken() (string, error) {
	bytes := make([]byte, 32)
	if _, err := rand.Read(bytes); err != nil {
		return "", err
	}
	return hex.EncodeToString(bytes), nil
}