package feichibridge

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

const feichiPublicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOwmFtEk1oJxDU0NI4kVO0Jx0X
nt+Abx+JKGUzcRzPwjUkd5z9Ice5rh87CCmj0XjZ5pPac6TtA3f0v5FqiK/kjQY5
XMLti4weJ4dcp1/q1O7PCYxRX8WgetGxwsjxGn+uoZOZkclN1PFS4wRnKEso6+G/
e60QGB29cZoo4jZnZwIDAQAB
-----END PUBLIC KEY-----`

type LoginCredentials struct {
	Username    string
	Password    string
	MaxAttempts int
	RetryDelay  time.Duration
}

type loginSession struct {
	credentials LoginCredentials
	solver      CaptchaSolver
	mu          sync.Mutex
	ready       atomic.Bool
	csrf        atomic.Value
	token       atomic.Value
}

type firstLoginEnvelope struct {
	Code any `json:"code"`
}

type captchaPayload struct {
	Src string `json:"src"`
}

type loginPayload struct {
	Token string `json:"token"`
}

func newLoginSession(credentials LoginCredentials, solver CaptchaSolver) (*loginSession, error) {
	credentials.Username = strings.TrimSpace(credentials.Username)
	if credentials.Username == "" || credentials.Password == "" {
		return nil, errors.New("Feichi username and password are required")
	}
	if solver == nil {
		return nil, errors.New("captcha solver is required")
	}
	if credentials.MaxAttempts <= 0 {
		credentials.MaxAttempts = 20
	}
	if credentials.RetryDelay <= 0 {
		credentials.RetryDelay = time.Second
	}
	session := &loginSession{credentials: credentials, solver: solver}
	session.csrf.Store("")
	session.token.Store("")
	return session, nil
}

func (s *loginSession) invalidate() {
	s.ready.Store(false)
}

func (s *loginSession) cookieHeader() string {
	var cookies []string
	if csrf := s.csrf.Load().(string); csrf != "" {
		cookies = append(cookies, "CSRF="+csrf)
	}
	if token := s.token.Load().(string); token != "" {
		cookies = append(cookies, "R_SESS="+token)
	}
	return strings.Join(cookies, "; ")
}

func (s *loginSession) csrfHeader() string {
	return s.csrf.Load().(string)
}

func (s *loginSession) ensure(ctx context.Context, client *APIClient) error {
	if s.ready.Load() {
		return nil
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.ready.Load() {
		return nil
	}
	var lastErr error
	for attempt := 1; attempt <= s.credentials.MaxAttempts; attempt++ {
		if err := s.loginAttempt(ctx, client); err == nil {
			s.ready.Store(true)
			return nil
		} else {
			lastErr = err
		}
		if attempt < s.credentials.MaxAttempts {
			timer := time.NewTimer(s.credentials.RetryDelay)
			select {
			case <-ctx.Done():
				timer.Stop()
				return ctx.Err()
			case <-timer.C:
			}
		}
	}
	return fmt.Errorf("Feichi automatic login failed after %d attempts: %w", s.credentials.MaxAttempts, lastErr)
}

func (s *loginSession) loginAttempt(ctx context.Context, client *APIClient) error {
	s.ready.Store(false)
	s.token.Store("")
	var first firstLoginEnvelope
	headers, err := client.rawJSON(ctx, http.MethodGet, "api/v1/first-login", nil, &first)
	if err != nil {
		return fmt.Errorf("initialize login session: %w", err)
	}
	if !apiCodeOK(first.Code) {
		return apiCodeError(first.Code)
	}
	csrf := strings.TrimSpace(headers.Get("x-api-csrf"))
	if csrf == "" {
		for _, rawCookie := range headers.Values("Set-Cookie") {
			cookie, parseErr := http.ParseSetCookie(rawCookie)
			if parseErr == nil && cookie.Name == "CSRF" {
				csrf = cookie.Value
				break
			}
		}
	}
	if csrf == "" {
		return errors.New("first-login response did not provide CSRF")
	}
	s.csrf.Store(csrf)

	var captcha objectEnvelope[captchaPayload]
	if _, err := client.rawJSON(ctx, http.MethodGet, "api/v1/login/randCode", nil, &captcha); err != nil {
		return fmt.Errorf("request captcha: %w", err)
	}
	if !apiCodeOK(captcha.Code) {
		return apiCodeError(captcha.Code)
	}
	encodedImage := captcha.Data.Src
	if comma := strings.IndexByte(encodedImage, ','); comma >= 0 {
		encodedImage = encodedImage[comma+1:]
	}
	image, err := base64.StdEncoding.DecodeString(encodedImage)
	if err != nil {
		return fmt.Errorf("decode captcha image: %w", err)
	}
	validCode, err := s.solver.Solve(ctx, image)
	if err != nil {
		return err
	}
	encrypted, err := encryptFeichiPassword(s.credentials.Password)
	if err != nil {
		return err
	}
	var login objectEnvelope[loginPayload]
	if _, err := client.rawJSON(ctx, http.MethodPost, "api/v1/login", map[string]string{
		"username":  s.credentials.Username,
		"password":  encrypted,
		"validCode": validCode,
	}, &login); err != nil {
		return fmt.Errorf("submit login: %w", err)
	}
	if !apiCodeOK(login.Code) {
		return apiCodeError(login.Code)
	}
	token := strings.TrimSpace(login.Data.Token)
	if token == "" {
		return errors.New("login succeeded without R_SESS token")
	}
	s.token.Store(token)
	return nil
}

func encryptFeichiPassword(password string) (string, error) {
	block, _ := pem.Decode([]byte(feichiPublicKey))
	if block == nil {
		return "", errors.New("decode Feichi RSA public key")
	}
	parsed, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return "", fmt.Errorf("parse Feichi RSA public key: %w", err)
	}
	publicKey, ok := parsed.(*rsa.PublicKey)
	if !ok {
		return "", errors.New("Feichi public key is not RSA")
	}
	encrypted, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(password))
	if err != nil {
		return "", fmt.Errorf("encrypt Feichi password: %w", err)
	}
	return base64.StdEncoding.EncodeToString(encrypted), nil
}
