package auth

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"golang.org/x/crypto/bcrypt"
	"senlinai-agent/backend/internal/models"
)

type Service struct {
	secret         string
	inviteTokenTTL time.Duration
	sessionTTL     time.Duration
}

func NewService(secret string) *Service {
	return &Service{secret: secret, inviteTokenTTL: 7 * 24 * time.Hour, sessionTTL: 24 * time.Hour}
}

func (s *Service) CreateInvite(adminID uint, email string) (string, error) {
	normalized := strings.ToLower(strings.TrimSpace(email))
	if normalized == "" {
		return "", errors.New("email is required")
	}
	return s.signToken(tokenPayload{
		Type:      "invite",
		Subject:   normalized,
		IssuerID:  adminID,
		ExpiresAt: time.Now().Add(s.inviteTokenTTL).Unix(),
	})
}

func (s *Service) RegisterWithInvite(token, email, displayName, password string) (*models.SenlinAgentUser, error) {
	payload, err := s.verifyToken(token, "invite")
	if err != nil {
		return nil, err
	}
	if payload.Subject != strings.ToLower(strings.TrimSpace(email)) {
		return nil, errors.New("invite email mismatch")
	}
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return nil, err
	}
	user := &models.SenlinAgentUser{
		Email:        payload.Subject,
		DisplayName:  strings.TrimSpace(displayName),
		PasswordHash: string(hash),
		Role:         "user",
	}
	return user, models.DBService.Create(user).Error
}

func (s *Service) Login(email, password string) (string, error) {
	var user models.SenlinAgentUser
	if err := models.DBService.Where("email = ?", strings.ToLower(strings.TrimSpace(email))).First(&user).Error; err != nil {
		return "", errors.New("invalid credentials")
	}
	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
		return "", errors.New("invalid credentials")
	}
	return s.signToken(tokenPayload{
		Type:      "session",
		Subject:   fmt.Sprintf("%d", user.ID),
		ExpiresAt: time.Now().Add(s.sessionTTL).Unix(),
	})
}

func (s *Service) VerifySession(token string) (uint, error) {
	payload, err := s.verifyToken(token, "session")
	if err != nil {
		return 0, err
	}
	var userID uint
	if _, err := fmt.Sscanf(payload.Subject, "%d", &userID); err != nil || userID == 0 {
		return 0, errors.New("invalid session subject")
	}
	return userID, nil
}

type tokenPayload struct {
	Type      string `json:"type"`
	Subject   string `json:"subject"`
	IssuerID  uint   `json:"issuer_id,omitempty"`
	ExpiresAt int64  `json:"expires_at"`
}

func (s *Service) signToken(payload tokenPayload) (string, error) {
	body, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}
	encoded := base64.RawURLEncoding.EncodeToString(body)
	return encoded + "." + s.sign(encoded), nil
}

func (s *Service) verifyToken(token string, expectedType string) (tokenPayload, error) {
	separator := strings.LastIndex(token, ".")
	if separator < 1 || separator == len(token)-1 {
		return tokenPayload{}, errors.New("invalid token")
	}
	encoded := token[:separator]
	signature := token[separator+1:]
	if !hmac.Equal([]byte(signature), []byte(s.sign(encoded))) {
		return tokenPayload{}, errors.New("invalid token signature")
	}
	body, err := base64.RawURLEncoding.DecodeString(encoded)
	if err != nil {
		return tokenPayload{}, errors.New("invalid token payload")
	}
	var payload tokenPayload
	if err := json.Unmarshal(body, &payload); err != nil {
		return tokenPayload{}, errors.New("invalid token payload")
	}
	if payload.Type != expectedType {
		return tokenPayload{}, errors.New("invalid token type")
	}
	if payload.ExpiresAt <= time.Now().Unix() {
		return tokenPayload{}, errors.New("token expired")
	}
	return payload, nil
}

func (s *Service) sign(value string) string {
	mac := hmac.New(sha256.New, []byte(s.secret))
	mac.Write([]byte(value))
	return hex.EncodeToString(mac.Sum(nil))
}
