package service

import (
	"encoding/json"
	"errors"
	"strings"
	"time"

	"juhe-factory/api/internal/model"
	"juhe-factory/api/internal/security"

	"github.com/google/uuid"
	"gorm.io/gorm"
)

var ErrUnauthorized = errors.New("账号或密码错误")
var ErrReauthFailed = errors.New("当前管理员密码错误")
var ErrAdminPasswordConfirmation = errors.New("两次输入的新密码不一致")
var ErrAdminPasswordLength = errors.New("新密码长度必须为 8～20 位")

type Auth struct {
	db        *gorm.DB
	passwords security.PasswordHasher
	tokens    security.TokenService
}

type TokenPair struct {
	AccessToken  string         `json:"access_token"`
	RefreshToken string         `json:"refresh_token"`
	ExpiresAt    time.Time      `json:"expires_at"`
	Admin        map[string]any `json:"admin"`
}

func NewAuth(db *gorm.DB, passwords security.PasswordHasher, tokens security.TokenService) *Auth {
	return &Auth{db: db, passwords: passwords, tokens: tokens}
}

func (s *Auth) Bootstrap(username, password string) error {
	if strings.TrimSpace(password) == "" {
		return nil
	}
	username = strings.TrimSpace(username)
	var admin model.AdminUser
	err := s.db.Where("username = ? AND deleted_at IS NULL", username).First(&admin).Error
	if err == nil {
		if s.passwords.Verify(admin.PasswordHash, password) {
			return nil
		}
		hash, hashErr := s.passwords.Hash(password)
		if hashErr != nil {
			return hashErr
		}
		return s.db.Model(&admin).Update("password_hash", hash).Error
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	var count int64
	if err := s.db.Model(&model.AdminUser{}).Count(&count).Error; err != nil || count > 0 {
		return err
	}
	hash, err := s.passwords.Hash(password)
	if err != nil {
		return err
	}
	return s.db.Create(&model.AdminUser{Username: username, PasswordHash: hash, Enabled: true}).Error
}

func (s *Auth) Login(username, password string) (*TokenPair, error) {
	var admin model.AdminUser
	if err := s.db.Where("username = ? AND deleted_at IS NULL", strings.TrimSpace(username)).First(&admin).Error; err != nil || !admin.Enabled || !s.passwords.Verify(admin.PasswordHash, password) {
		return nil, ErrUnauthorized
	}
	return s.issue(&admin, true)
}

func (s *Auth) Refresh(refreshToken string) (*TokenPair, error) {
	hash := security.HashToken(refreshToken)
	var record model.AdminRefreshToken
	if err := s.db.Where("token_hash = ? AND revoked_at IS NULL AND expires_at > ?", hash, time.Now()).First(&record).Error; err != nil {
		return nil, ErrUnauthorized
	}
	var admin model.AdminUser
	if err := s.db.First(&admin, "id = ? AND enabled = true", record.AdminID).Error; err != nil {
		return nil, ErrUnauthorized
	}
	now := time.Now()
	if err := s.db.Model(&record).Update("revoked_at", now).Error; err != nil {
		return nil, err
	}
	return s.issue(&admin, false)
}

func (s *Auth) Logout(refreshToken string) error {
	if refreshToken == "" {
		return nil
	}
	now := time.Now()
	return s.db.Model(&model.AdminRefreshToken{}).Where("token_hash = ? AND revoked_at IS NULL", security.HashToken(refreshToken)).Update("revoked_at", now).Error
}

func (s *Auth) Authenticate(raw string) (*model.AdminUser, error) {
	claims, err := s.tokens.ParseAccessToken(raw)
	if err != nil {
		return nil, ErrUnauthorized
	}
	id, err := uuid.Parse(claims.AdminID)
	if err != nil {
		return nil, ErrUnauthorized
	}
	var admin model.AdminUser
	if err := s.db.First(&admin, "id = ? AND enabled = true", id).Error; err != nil {
		return nil, ErrUnauthorized
	}
	return &admin, nil
}

func (s *Auth) ChangePassword(adminID uuid.UUID, currentPassword, newPassword, confirmPassword string) error {
	if newPassword != confirmPassword {
		return ErrAdminPasswordConfirmation
	}
	if len(newPassword) < 8 || len(newPassword) > 20 {
		return ErrAdminPasswordLength
	}
	var admin model.AdminUser
	if err := s.db.Where("id = ? AND enabled = true AND deleted_at IS NULL", adminID).First(&admin).Error; err != nil {
		return ErrUnauthorized
	}
	if !s.passwords.Verify(admin.PasswordHash, currentPassword) {
		return ErrReauthFailed
	}
	hash, err := s.passwords.Hash(newPassword)
	if err != nil {
		return err
	}
	now := time.Now()
	return s.db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Model(&admin).Update("password_hash", hash).Error; err != nil {
			return err
		}
		return tx.Model(&model.AdminRefreshToken{}).
			Where("admin_id = ? AND revoked_at IS NULL", adminID).
			Update("revoked_at", now).Error
	})
}

func (s *Auth) Audit(admin *model.AdminUser, action, resource, resourceID, reason, ip, traceID string, detail any) error {
	return WriteAudit(s.db, admin, action, resource, resourceID, reason, ip, traceID, detail)
}

func (s *Auth) issue(admin *model.AdminUser, updateLogin bool) (*TokenPair, error) {
	access, expiresAt, err := s.tokens.NewAccessToken(admin.ID.String(), admin.Username)
	if err != nil {
		return nil, err
	}
	refresh, hash, refreshExpires, err := s.tokens.NewRefreshToken()
	if err != nil {
		return nil, err
	}
	record := model.AdminRefreshToken{AdminID: admin.ID, TokenHash: hash, ExpiresAt: refreshExpires}
	if err := s.db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Create(&record).Error; err != nil {
			return err
		}
		if updateLogin {
			return tx.Model(admin).Update("last_login_at", time.Now()).Error
		}
		return nil
	}); err != nil {
		return nil, err
	}
	return &TokenPair{AccessToken: access, RefreshToken: refresh, ExpiresAt: expiresAt, Admin: map[string]any{"id": admin.ID, "username": admin.Username}}, nil
}

func WriteAudit(db *gorm.DB, admin *model.AdminUser, action, resource, resourceID, reason, ip, traceID string, detail any) error {
	data, _ := json.Marshal(detail)
	log := model.AdminAuditLog{AdminID: &admin.ID, AdminUsername: admin.Username, Action: action, ResourceType: resource, ResourceID: resourceID, Reason: reason, Detail: string(data), IPAddress: ip, TraceID: traceID}
	return db.Create(&log).Error
}
