package auth

import (
	"bytes"
	"crypto/rand"
	"crypto/subtle"
	"encoding/hex"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/crypto/bcrypt"

	"ymhut-box/server/feedback-mailer/internal/config"
)

const (
	sessionCookie = "ymhut_feedback_session"
	captchaTTL    = 5 * time.Minute
	sessionTTL    = 12 * time.Hour
)

type Service struct {
	cfg      *config.Config
	mu       sync.Mutex
	captchas map[string]captchaEntry
	sessions map[string]sessionEntry
}

type captchaEntry struct {
	Answer    string
	ExpiresAt time.Time
}

type sessionEntry struct {
	ExpiresAt time.Time
	CSRFToken string
}

type Captcha struct {
	ID       string
	ImagePNG []byte
}

func NewService(cfg *config.Config) *Service {
	return &Service{
		cfg:      cfg,
		captchas: map[string]captchaEntry{},
		sessions: map[string]sessionEntry{},
	}
}

func (s *Service) NewCaptcha() (Captcha, error) {
	answer := randomDigits(5)
	id := randomToken(16)
	imageBytes, err := renderCaptcha(answer)
	if err != nil {
		return Captcha{}, err
	}

	s.mu.Lock()
	s.cleanupLocked()
	s.captchas[id] = captchaEntry{Answer: answer, ExpiresAt: time.Now().Add(captchaTTL)}
	s.mu.Unlock()
	return Captcha{ID: id, ImagePNG: imageBytes}, nil
}

func (s *Service) Login(password, captchaID, captchaAnswer string) (string, string, bool) {
	if !s.consumeCaptcha(captchaID, captchaAnswer) {
		return "", "", false
	}
	if !s.VerifyPassword(password) {
		return "", "", false
	}
	sessionID := randomToken(32)
	csrf := randomToken(32)
	s.mu.Lock()
	s.cleanupLocked()
	s.sessions[sessionID] = sessionEntry{ExpiresAt: time.Now().Add(sessionTTL), CSRFToken: csrf}
	s.mu.Unlock()
	return sessionID, csrf, true
}

func (s *Service) Logout(c *gin.Context) {
	sessionID, _ := c.Cookie(sessionCookie)
	s.mu.Lock()
	delete(s.sessions, sessionID)
	s.mu.Unlock()
	clearCookie(c)
}

func (s *Service) RequireAuth(c *gin.Context) {
	sessionID, err := c.Cookie(sessionCookie)
	if err != nil || sessionID == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"ok": false, "error": "UNAUTHORIZED", "message": "Login required"})
		c.Abort()
		return
	}
	csrf, ok := s.SessionCSRF(sessionID)
	if !ok {
		clearCookie(c)
		c.JSON(http.StatusUnauthorized, gin.H{"ok": false, "error": "UNAUTHORIZED", "message": "Login required"})
		c.Abort()
		return
	}
	c.Set("csrf", csrf)
	c.Next()
}

func (s *Service) RequireCSRF(c *gin.Context) {
	if c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead || c.Request.Method == http.MethodOptions {
		c.Next()
		return
	}
	expected, _ := c.Get("csrf")
	actual := c.GetHeader("X-CSRF-Token")
	if expected == nil || actual == "" || subtle.ConstantTimeCompare([]byte(expected.(string)), []byte(actual)) != 1 {
		c.JSON(http.StatusForbidden, gin.H{"ok": false, "error": "CSRF_INVALID", "message": "Invalid CSRF token"})
		c.Abort()
		return
	}
	c.Next()
}

func (s *Service) SetSessionCookie(c *gin.Context, sessionID string) {
	c.SetSameSite(http.SameSiteLaxMode)
	c.SetCookie(sessionCookie, sessionID, int(sessionTTL.Seconds()), "/", "", false, true)
}

func (s *Service) SessionCSRF(sessionID string) (string, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.cleanupLocked()
	session, ok := s.sessions[sessionID]
	if !ok {
		return "", false
	}
	return session.CSRFToken, true
}

func (s *Service) VerifyPassword(password string) bool {
	password = strings.TrimSpace(password)
	if password == "" {
		return false
	}
	hash := strings.TrimSpace(s.cfg.AdminPasswordHash)
	if hash != "" && verifyBcrypt(hash, password) {
		return true
	}
	plain := strings.TrimSpace(s.cfg.AdminPassword)
	return plain != "" && subtle.ConstantTimeCompare([]byte(plain), []byte(password)) == 1
}

func (s *Service) consumeCaptcha(id, answer string) bool {
	id = strings.TrimSpace(id)
	answer = strings.TrimSpace(answer)
	s.mu.Lock()
	defer s.mu.Unlock()
	s.cleanupLocked()
	entry, ok := s.captchas[id]
	if ok {
		delete(s.captchas, id)
	}
	if !ok || time.Now().After(entry.ExpiresAt) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(strings.ToLower(entry.Answer)), []byte(strings.ToLower(answer))) == 1
}

func (s *Service) cleanupLocked() {
	now := time.Now()
	for id, entry := range s.captchas {
		if now.After(entry.ExpiresAt) {
			delete(s.captchas, id)
		}
	}
	for id, entry := range s.sessions {
		if now.After(entry.ExpiresAt) {
			delete(s.sessions, id)
		}
	}
}

func verifyBcrypt(hash, password string) bool {
	candidates := []string{hash}
	if strings.HasPrefix(hash, "$2y$") {
		candidates = append(candidates, "$2a$"+strings.TrimPrefix(hash, "$2y$"))
	}
	for _, candidate := range candidates {
		if bcrypt.CompareHashAndPassword([]byte(candidate), []byte(password)) == nil {
			return true
		}
	}
	return false
}

func clearCookie(c *gin.Context) {
	c.SetSameSite(http.SameSiteLaxMode)
	c.SetCookie(sessionCookie, "", -1, "/", "", false, true)
}

func randomDigits(count int) string {
	data := make([]byte, count)
	if _, err := rand.Read(data); err != nil {
		return "12345"
	}
	var builder strings.Builder
	for _, value := range data {
		builder.WriteByte('0' + value%10)
	}
	return builder.String()
}

func randomToken(bytesLen int) string {
	data := make([]byte, bytesLen)
	if _, err := rand.Read(data); err != nil {
		return hex.EncodeToString([]byte(time.Now().Format(time.RFC3339Nano)))
	}
	return hex.EncodeToString(data)
}

func renderCaptcha(answer string) ([]byte, error) {
	img := image.NewRGBA(image.Rect(0, 0, 180, 64))
	draw.Draw(img, img.Bounds(), &image.Uniform{color.RGBA{245, 248, 252, 255}}, image.Point{}, draw.Src)

	for i := 0; i < 24; i++ {
		x := (i*37 + 13) % 180
		y := (i*19 + 7) % 64
		img.Set(x, y, color.RGBA{102, 120, 145, 255})
	}

	for index, digit := range answer {
		drawDigit(img, int(digit-'0'), 18+index*32, 13, color.RGBA{28, 72, 130, 255})
	}

	var buffer bytes.Buffer
	if err := png.Encode(&buffer, img); err != nil {
		return nil, err
	}
	return buffer.Bytes(), nil
}

var segments = [10][7]bool{
	{true, true, true, true, true, true, false},
	{false, true, true, false, false, false, false},
	{true, true, false, true, true, false, true},
	{true, true, true, true, false, false, true},
	{false, true, true, false, false, true, true},
	{true, false, true, true, false, true, true},
	{true, false, true, true, true, true, true},
	{true, true, true, false, false, false, false},
	{true, true, true, true, true, true, true},
	{true, true, true, true, false, true, true},
}

func drawDigit(img *image.RGBA, digit, x, y int, col color.Color) {
	if digit < 0 || digit > 9 {
		return
	}
	thick := 4
	width := 22
	height := 36
	drawSegment := func(rect image.Rectangle) {
		draw.Draw(img, rect, &image.Uniform{col}, image.Point{}, draw.Src)
	}
	if segments[digit][0] {
		drawSegment(image.Rect(x+thick, y, x+width-thick, y+thick))
	}
	if segments[digit][1] {
		drawSegment(image.Rect(x+width-thick, y+thick, x+width, y+height/2))
	}
	if segments[digit][2] {
		drawSegment(image.Rect(x+width-thick, y+height/2, x+width, y+height-thick))
	}
	if segments[digit][3] {
		drawSegment(image.Rect(x+thick, y+height-thick, x+width-thick, y+height))
	}
	if segments[digit][4] {
		drawSegment(image.Rect(x, y+height/2, x+thick, y+height-thick))
	}
	if segments[digit][5] {
		drawSegment(image.Rect(x, y+thick, x+thick, y+height/2))
	}
	if segments[digit][6] {
		drawSegment(image.Rect(x+thick, y+height/2-thick/2, x+width-thick, y+height/2+thick/2))
	}
}
