package service

import (
	"context"
	"errors"
	"fmt"
	"time"

	datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
	"github.com/pocket-id/pocket-id/backend/internal/utils/email"

	"github.com/pocket-id/pocket-id/backend/internal/common"
	"github.com/pocket-id/pocket-id/backend/internal/dto"
	"github.com/pocket-id/pocket-id/backend/internal/model"
	"github.com/pocket-id/pocket-id/backend/internal/utils"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

const staticApiKeyUserID = "00000000-0000-0000-0000-000000000000"

type ApiKeyService struct {
	db           *gorm.DB
	emailService *EmailService
}

func NewApiKeyService(ctx context.Context, db *gorm.DB, emailService *EmailService) (*ApiKeyService, error) {
	s := &ApiKeyService{db: db, emailService: emailService}

	if common.EnvConfig.StaticApiKey == "" {
		err := s.deleteStaticApiKeyUser(ctx)
		if err != nil {
			return nil, err
		}
	}

	return s, nil

}

func (s *ApiKeyService) ListApiKeys(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.ApiKey, utils.PaginationResponse, error) {
	query := s.db.
		WithContext(ctx).
		Where("user_id = ?", userID).
		Model(&model.ApiKey{})

	var apiKeys []model.ApiKey
	pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &apiKeys)
	if err != nil {
		return nil, utils.PaginationResponse{}, err
	}

	return apiKeys, pagination, nil
}

func (s *ApiKeyService) CreateApiKey(ctx context.Context, userID string, input dto.ApiKeyCreateDto) (model.ApiKey, string, error) {
	// Check if expiration is in the future
	if !input.ExpiresAt.ToTime().After(time.Now()) {
		return model.ApiKey{}, "", &common.APIKeyExpirationDateError{}
	}

	// Generate a secure random API key
	token, err := utils.GenerateRandomAlphanumericString(32)
	if err != nil {
		return model.ApiKey{}, "", err
	}

	apiKey := model.ApiKey{
		Name:        input.Name,
		Key:         utils.CreateSha256Hash(token), // Hash the token for storage
		Description: input.Description,
		ExpiresAt:   input.ExpiresAt,
		UserID:      userID,
	}

	err = s.db.
		WithContext(ctx).
		Create(&apiKey).
		Error
	if err != nil {
		if errors.Is(err, gorm.ErrDuplicatedKey) {
			return model.ApiKey{}, "", &common.AlreadyInUseError{Property: "API key name"}
		}
		return model.ApiKey{}, "", err
	}

	// Return the raw token only once - it cannot be retrieved later
	return apiKey, token, nil
}

func (s *ApiKeyService) RenewApiKey(ctx context.Context, userID, apiKeyID string, expiration time.Time) (model.ApiKey, string, error) {
	// Check if expiration is in the future
	if !expiration.After(time.Now()) {
		return model.ApiKey{}, "", &common.APIKeyExpirationDateError{}
	}

	tx := s.db.Begin()
	defer tx.Rollback()

	var apiKey model.ApiKey
	err := tx.
		WithContext(ctx).
		Model(&model.ApiKey{}).
		Where("id = ? AND user_id = ?", apiKeyID, userID).
		First(&apiKey).
		Error

	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return model.ApiKey{}, "", &common.APIKeyNotFoundError{}
		}
		return model.ApiKey{}, "", err
	}

	// Only allow renewal if the key has already expired
	if apiKey.ExpiresAt.ToTime().After(time.Now()) {
		return model.ApiKey{}, "", &common.APIKeyNotExpiredError{}
	}

	// Generate a secure random API key
	token, err := utils.GenerateRandomAlphanumericString(32)
	if err != nil {
		return model.ApiKey{}, "", err
	}

	apiKey.Key = utils.CreateSha256Hash(token)
	apiKey.ExpiresAt = datatype.DateTime(expiration)

	err = tx.WithContext(ctx).Save(&apiKey).Error
	if err != nil {
		return model.ApiKey{}, "", err
	}

	if err := tx.Commit().Error; err != nil {
		return model.ApiKey{}, "", err
	}

	return apiKey, token, nil
}

func (s *ApiKeyService) RevokeApiKey(ctx context.Context, userID, apiKeyID string) error {
	var apiKey model.ApiKey
	err := s.db.
		WithContext(ctx).
		Where("id = ? AND user_id = ?", apiKeyID, userID).
		Delete(&apiKey).
		Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return &common.APIKeyNotFoundError{}
		}
		return err
	}

	return nil
}

func (s *ApiKeyService) ValidateApiKey(ctx context.Context, apiKey string) (model.User, error) {
	if apiKey == "" {
		return model.User{}, &common.NoAPIKeyProvidedError{}
	}

	if common.EnvConfig.StaticApiKey != "" && apiKey == common.EnvConfig.StaticApiKey {
		return s.initStaticApiKeyUser(ctx)
	}

	now := time.Now()
	hashedKey := utils.CreateSha256Hash(apiKey)

	var key model.ApiKey
	err := s.db.
		WithContext(ctx).
		Model(&model.ApiKey{}).
		Clauses(clause.Returning{}).
		Where("key = ? AND expires_at > ?", hashedKey, datatype.DateTime(now)).
		Updates(&model.ApiKey{
			LastUsedAt: new(datatype.DateTime(now)),
		}).
		Preload("User").
		First(&key).
		Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return model.User{}, &common.InvalidAPIKeyError{}
		}

		return model.User{}, err
	}

	return key.User, nil
}

func (s *ApiKeyService) ListExpiringApiKeys(ctx context.Context, daysAhead int) ([]model.ApiKey, error) {
	var keys []model.ApiKey
	now := time.Now()
	cutoff := now.AddDate(0, 0, daysAhead)

	err := s.db.
		WithContext(ctx).
		Preload("User").
		Where("expires_at > ? AND expires_at <= ? AND expiration_email_sent = ?", datatype.DateTime(now), datatype.DateTime(cutoff), false).
		Find(&keys).
		Error

	return keys, err
}

func (s *ApiKeyService) SendApiKeyExpiringSoonEmail(ctx context.Context, apiKey model.ApiKey) error {
	if apiKey.User.Email == nil {
		return &common.UserEmailNotSetError{}
	}

	err := SendEmail(ctx, s.emailService, email.Address{
		Name:  apiKey.User.FullName(),
		Email: *apiKey.User.Email,
	}, ApiKeyExpiringSoonTemplate, &ApiKeyExpiringSoonTemplateData{
		ApiKeyName: apiKey.Name,
		ExpiresAt:  apiKey.ExpiresAt.ToTime(),
		Name:       apiKey.User.FirstName,
	})
	if err != nil {
		return fmt.Errorf("error sending notification email: %w", err)
	}

	// Mark the API key as having had an expiration email sent
	err = s.db.WithContext(ctx).
		Model(&model.ApiKey{}).
		Where("id = ?", apiKey.ID).
		Update("expiration_email_sent", true).
		Error
	if err != nil {
		return fmt.Errorf("error recording expiration sent email in database: %w", err)
	}

	return nil
}

func (s *ApiKeyService) initStaticApiKeyUser(ctx context.Context) (user model.User, err error) {
	err = s.db.
		WithContext(ctx).
		First(&user, "id = ?", staticApiKeyUserID).
		Error

	if err == nil {
		return user, nil
	}

	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return model.User{}, err
	}

	usernameSuffix, err := utils.GenerateRandomAlphanumericString(6)
	if err != nil {
		return model.User{}, err
	}

	user = model.User{
		Base: model.Base{
			ID: staticApiKeyUserID,
		},
		FirstName:   "Static API User",
		Username:    "static-api-user-" + usernameSuffix,
		DisplayName: "Static API User",
		IsAdmin:     true,
	}

	err = s.db.
		WithContext(ctx).
		Create(&user).
		Error

	return user, err
}

func (s *ApiKeyService) deleteStaticApiKeyUser(ctx context.Context) error {
	return s.db.
		WithContext(ctx).
		Delete(&model.User{}, "id = ?", staticApiKeyUserID).
		Error
}
