package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"image"
	"image/color"
	_ "image/jpeg"
	"log"
	"math"
	mathrand "math/rand"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"sync"
)

type sliderPuzzleV2 struct {
	Image    image.Image
	Size     int
	Swaps    []int
	Attempts int
}

type sliderGuessV2 struct {
	Index         int
	Swaps         []int
	Score         int64
	ScoreRGB      int64
	ScoreLuma     int64
	ScoreText     float64
	ConsensusRank int
}

func (s *captchaV2Session) solveSliderCaptcha(
	sessionToken string,
	browserFP string,
	hash string,
	settings string,
	debugInfo string,
) (string, error) {
	values := [][2]string{
		{"session_token", sessionToken},
		{"domain", "vk.com"},
		{"adFp", ""},
		{"access_token", ""},
		{"captcha_settings", settings},
	}

	resp, err := s.captchaRequest("captchaNotRobot.getContent", values)
	if err != nil {
		return "", fmt.Errorf("slider getContent failed: %w", err)
	}
	puzzle, err := parseSliderPuzzleV2(resp)
	if err != nil {
		return "", err
	}
	log.Printf("[КАПЧА] v2 slider puzzle decoded: grid=%d attempts=%d swaps=%d", puzzle.Size, puzzle.Attempts, len(puzzle.Swaps))

	guesses, err := rankSliderGuessesV2(puzzle.Image, puzzle.Size, puzzle.Swaps)
	if err != nil {
		return "", err
	}

	limit := puzzle.Attempts
	if limit > len(guesses) {
		limit = len(guesses)
	}
	if limit <= 0 {
		return "", errors.New("slider has no attempts available")
	}
	log.Printf("[КАПЧА] v2 slider guesses ranked: total=%d limit=%d", len(guesses), limit)

	deviceJSON := captchaV2DeviceInfo
	if s.savedProfile != nil && strings.TrimSpace(s.savedProfile.DeviceJSON) != "" {
		deviceJSON = s.savedProfile.DeviceJSON
	}
	if _, err := s.captchaRequest("captchaNotRobot.componentDone", [][2]string{
		{"session_token", sessionToken},
		{"domain", "vk.com"},
		{"adFp", ""},
		{"access_token", ""},
		{"browser_fp", browserFP},
		{"device", deviceJSON},
	}); err != nil {
		return "", fmt.Errorf("captcha componentDone failed: %w", err)
	}

	for i := 0; i < limit; i++ {
		log.Printf("[КАПЧА] v2 slider attempt %d/%d (guess #%d)", i+1, limit, guesses[i].Index)
		answerData, err := json.Marshal(struct {
			Value []int `json:"value"`
		}{Value: guesses[i].Swaps})
		if err != nil {
			return "", err
		}
		check, err := s.performCaptchaCheck(
			sessionToken,
			browserFP,
			hash,
			string(answerData),
			buildSliderCursorV2(guesses[i].Index, len(guesses)),
			debugInfo,
		)
		if err != nil {
			return "", err
		}
		if strings.EqualFold(check.Status, "ok") {
			if check.SuccessToken == "" {
				return "", errors.New("captcha success token not found")
			}
			log.Printf("[КАПЧА] v2 slider accepted on attempt %d", i+1)
			return check.SuccessToken, nil
		}
		if strings.EqualFold(check.Status, "error_limit") {
			return "", errCaptchaV2RateLimit
		}
	}
	return "", errors.New("slider guesses exhausted")
}

func parseSliderPuzzleV2(raw map[string]any) (*sliderPuzzleV2, error) {
	resp, ok := raw["response"].(map[string]any)
	if !ok {
		return nil, fmt.Errorf("invalid slider content response: %v", raw)
	}
	status := captchaV2StringifyAny(resp["status"])
	if !strings.EqualFold(status, "ok") {
		return nil, fmt.Errorf("slider getContent status: %s", status)
	}
	rawImage := captchaV2StringifyAny(resp["image"])
	if rawImage == "" {
		return nil, errors.New("slider image missing")
	}
	rawSteps, ok := resp["steps"].([]any)
	if !ok {
		return nil, errors.New("slider steps missing")
	}
	steps := make([]int, 0, len(rawSteps))
	for _, item := range rawSteps {
		switch v := item.(type) {
		case float64:
			steps = append(steps, int(v))
		case int:
			steps = append(steps, v)
		case string:
			n, err := strconv.Atoi(strings.TrimSpace(v))
			if err != nil {
				return nil, fmt.Errorf("invalid numeric value: %v", item)
			}
			steps = append(steps, n)
		default:
			return nil, fmt.Errorf("invalid numeric value: %v", item)
		}
	}
	size, swaps, attempts, err := splitSliderStepsV2(steps)
	if err != nil {
		return nil, err
	}
	data, err := base64.StdEncoding.DecodeString(rawImage)
	if err != nil {
		return nil, fmt.Errorf("decode slider image: %w", err)
	}
	img, _, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		return nil, fmt.Errorf("decode slider image: %w", err)
	}
	return &sliderPuzzleV2{Image: img, Size: size, Swaps: swaps, Attempts: attempts}, nil
}

func splitSliderStepsV2(steps []int) (int, []int, int, error) {
	if len(steps) < 3 {
		return 0, nil, 0, errors.New("slider steps payload too short")
	}
	size := steps[0]
	if size <= 0 {
		return 0, nil, 0, fmt.Errorf("invalid slider size: %d", size)
	}
	tail := append([]int(nil), steps[1:]...)
	attempts := 4
	if len(tail)%2 != 0 {
		attempts = tail[len(tail)-1]
		tail = tail[:len(tail)-1]
		log.Printf("[КАПЧА] v2 slider payload had odd-length tail; fallback attempts=%d", attempts)
	}
	if attempts <= 0 {
		attempts = 4
	}
	if len(tail) == 0 || len(tail)%2 != 0 {
		return 0, nil, 0, errors.New("invalid slider swap payload")
	}
	return size, tail, attempts, nil
}

func rankSliderGuessesV2(img image.Image, gridSize int, swaps []int) ([]sliderGuessV2, error) {
	candidateCount := len(swaps) / 2
	if candidateCount == 0 {
		return nil, errors.New("slider has no candidates")
	}

	guesses := make([]sliderGuessV2, candidateCount)
	for idx := 1; idx <= candidateCount; idx++ {
		active := activeSwapsForIndexV2(swaps, idx)
		mapping, err := applySliderSwapsV2(gridSize, active)
		if err != nil {
			return nil, err
		}
		guesses[idx-1] = sliderGuessV2{Index: idx, Swaps: active}
		guesses[idx-1].ScoreLuma = seamScoreLumaV2(img, gridSize, mapping)
	}

	lumaOrder := append([]sliderGuessV2(nil), guesses...)
	sort.SliceStable(lumaOrder, func(i, j int) bool {
		if lumaOrder[i].ScoreLuma == lumaOrder[j].ScoreLuma {
			return lumaOrder[i].Index < lumaOrder[j].Index
		}
		return lumaOrder[i].ScoreLuma < lumaOrder[j].ScoreLuma
	})
	lumaRank := make(map[int]int, candidateCount)
	for rank, g := range lumaOrder {
		lumaRank[g.Index] = rank
	}

	stage2Count := candidateCount
	if stage2Count > 12 {
		stage2Count = 12
	}
	stage2Set := make(map[int]struct{}, stage2Count)
	for i := 0; i < stage2Count; i++ {
		stage2Set[lumaOrder[i].Index] = struct{}{}
	}

	type stage2Result struct {
		index int
		rgb   int64
		text  float64
		err   error
	}
	jobs := make([]int, 0, stage2Count)
	for idx := range stage2Set {
		jobs = append(jobs, idx)
	}
	jobCh := make(chan int, len(jobs))
	resCh := make(chan stage2Result, len(jobs))

	workers := runtime.NumCPU()
	if workers < 1 {
		workers = 1
	}
	if workers > len(jobs) {
		workers = len(jobs)
	}
	var wg sync.WaitGroup
	for w := 0; w < workers; w++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for index := range jobCh {
				mapping, err := applySliderSwapsV2(gridSize, guesses[index-1].Swaps)
				if err != nil {
					resCh <- stage2Result{index: index, err: err}
					continue
				}
				rgb, text := seamScoreRGBTextV2(img, gridSize, mapping)
				resCh <- stage2Result{index: index, rgb: rgb, text: text}
			}
		}()
	}
	for _, idx := range jobs {
		jobCh <- idx
	}
	close(jobCh)
	wg.Wait()
	close(resCh)
	for r := range resCh {
		if r.err != nil {
			return nil, r.err
		}
		g := &guesses[r.index-1]
		g.ScoreRGB = r.rgb
		g.ScoreText = r.text
	}

	stage2 := make([]sliderGuessV2, 0, stage2Count)
	for _, g := range guesses {
		if _, ok := stage2Set[g.Index]; ok {
			stage2 = append(stage2, g)
		}
	}

	rgbOrder := append([]sliderGuessV2(nil), stage2...)
	sort.SliceStable(rgbOrder, func(i, j int) bool {
		if rgbOrder[i].ScoreRGB == rgbOrder[j].ScoreRGB {
			return rgbOrder[i].Index < rgbOrder[j].Index
		}
		return rgbOrder[i].ScoreRGB < rgbOrder[j].ScoreRGB
	})
	rgbRank := make(map[int]int, len(rgbOrder))
	for rank, g := range rgbOrder {
		rgbRank[g.Index] = rank
	}

	textOrder := append([]sliderGuessV2(nil), stage2...)
	sort.SliceStable(textOrder, func(i, j int) bool {
		if textOrder[i].ScoreText == textOrder[j].ScoreText {
			return textOrder[i].Index < textOrder[j].Index
		}
		return textOrder[i].ScoreText < textOrder[j].ScoreText
	})
	textRank := make(map[int]int, len(textOrder))
	for rank, g := range textOrder {
		textRank[g.Index] = rank
	}

	for i := range guesses {
		g := &guesses[i]
		g.ConsensusRank = lumaRank[g.Index]
		if _, ok := stage2Set[g.Index]; ok {
			g.ConsensusRank += rgbRank[g.Index] + textRank[g.Index]
		} else {
			g.ConsensusRank += candidateCount
		}
		g.Score = int64(g.ConsensusRank)
	}

	sort.SliceStable(guesses, func(i, j int) bool {
		if guesses[i].ConsensusRank == guesses[j].ConsensusRank {
			if guesses[i].ScoreLuma == guesses[j].ScoreLuma {
				return guesses[i].Index < guesses[j].Index
			}
			return guesses[i].ScoreLuma < guesses[j].ScoreLuma
		}
		return guesses[i].ConsensusRank < guesses[j].ConsensusRank
	})
	return guesses, nil
}

func activeSwapsForIndexV2(swaps []int, index int) []int {
	if index <= 0 {
		return []int{}
	}
	end := index * 2
	if end > len(swaps) {
		end = len(swaps)
	}
	return append([]int(nil), swaps[:end]...)
}

func applySliderSwapsV2(gridSize int, swaps []int) ([]int, error) {
	tileCount := gridSize * gridSize
	if tileCount <= 0 {
		return nil, fmt.Errorf("invalid slider tile count: %d", tileCount)
	}
	if len(swaps)%2 != 0 {
		return nil, fmt.Errorf("invalid slider swaps length: %d", len(swaps))
	}
	mapping := make([]int, tileCount)
	for i := range mapping {
		mapping[i] = i
	}
	for i := 0; i < len(swaps); i += 2 {
		left := swaps[i]
		right := swaps[i+1]
		if left < 0 || right < 0 || left >= tileCount || right >= tileCount {
			return nil, fmt.Errorf("slider step out of range: %d,%d", left, right)
		}
		mapping[left], mapping[right] = mapping[right], mapping[left]
	}
	return mapping, nil
}

func sliderTileRect(bounds image.Rectangle, gridSize int, index int) image.Rectangle {
	w := bounds.Dx() / gridSize
	h := bounds.Dy() / gridSize
	col := index % gridSize
	row := index / gridSize
	return image.Rect(
		bounds.Min.X+col*w,
		bounds.Min.Y+row*h,
		bounds.Min.X+(col+1)*w,
		bounds.Min.Y+(row+1)*h,
	)
}

func pixelDiff(a, b color.Color) int64 {
	ar, ag, ab, _ := a.RGBA()
	br, bg, bb, _ := b.RGBA()
	dr := int64(ar>>8) - int64(br>>8)
	dg := int64(ag>>8) - int64(bg>>8)
	db := int64(ab>>8) - int64(bb>>8)
	if dr < 0 {
		dr = -dr
	}
	if dg < 0 {
		dg = -dg
	}
	if db < 0 {
		db = -db
	}
	return dr + dg + db
}

func seamScoreLumaV2(img image.Image, gridSize int, mapping []int) int64 {
	bounds := img.Bounds()
	var score int64
	for row := 0; row < gridSize; row++ {
		for col := 0; col < gridSize-1; col++ {
			leftIdx := row*gridSize + col
			rightIdx := leftIdx + 1
			leftDst := sliderTileRect(bounds, gridSize, leftIdx)
			rightDst := sliderTileRect(bounds, gridSize, rightIdx)
			leftSrc := sliderTileRect(bounds, gridSize, mapping[leftIdx])
			rightSrc := sliderTileRect(bounds, gridSize, mapping[rightIdx])
			h := leftDst.Dy()
			if rightDst.Dy() < h {
				h = rightDst.Dy()
			}
			for y := 0; y < h; y++ {
				yy := leftDst.Min.Y + y
				a := sampleLumaMappedV2(img, leftDst, leftSrc, leftDst.Max.X-1, yy)
				b := sampleLumaMappedV2(img, rightDst, rightSrc, rightDst.Min.X, yy)
				score += int64(absIntV2(int(a) - int(b)))
			}
		}
	}
	for row := 0; row < gridSize-1; row++ {
		for col := 0; col < gridSize; col++ {
			topIdx := row*gridSize + col
			bottomIdx := (row+1)*gridSize + col
			topDst := sliderTileRect(bounds, gridSize, topIdx)
			bottomDst := sliderTileRect(bounds, gridSize, bottomIdx)
			topSrc := sliderTileRect(bounds, gridSize, mapping[topIdx])
			bottomSrc := sliderTileRect(bounds, gridSize, mapping[bottomIdx])
			w := topDst.Dx()
			if bottomDst.Dx() < w {
				w = bottomDst.Dx()
			}
			for x := 0; x < w; x++ {
				xx := topDst.Min.X + x
				a := sampleLumaMappedV2(img, topDst, topSrc, xx, topDst.Max.Y-1)
				b := sampleLumaMappedV2(img, bottomDst, bottomSrc, xx, bottomDst.Min.Y)
				score += int64(absIntV2(int(a) - int(b)))
			}
		}
	}
	return score
}

func seamScoreRGBTextV2(img image.Image, gridSize int, mapping []int) (int64, float64) {
	bounds := img.Bounds()
	height := float64(bounds.Dy())
	textCenters := []float64{
		float64(bounds.Min.Y) + 0.2*height,
		float64(bounds.Min.Y) + 0.5*height,
		float64(bounds.Min.Y) + 0.8*height,
	}
	sigma := height * 0.14
	if sigma < 1.0 {
		sigma = 1.0
	}
	weight := func(y int) float64 {
		yf := float64(y)
		best := absFloatV2(yf - textCenters[0])
		for i := 1; i < len(textCenters); i++ {
			d := absFloatV2(yf - textCenters[i])
			if d < best {
				best = d
			}
		}
		return 1 + 3*math.Exp(-(best*best)/(2*sigma*sigma))
	}

	var rgbScore int64
	var textScore float64
	for row := 0; row < gridSize; row++ {
		for col := 0; col < gridSize-1; col++ {
			leftIdx := row*gridSize + col
			rightIdx := leftIdx + 1
			leftDst := sliderTileRect(bounds, gridSize, leftIdx)
			rightDst := sliderTileRect(bounds, gridSize, rightIdx)
			leftSrc := sliderTileRect(bounds, gridSize, mapping[leftIdx])
			rightSrc := sliderTileRect(bounds, gridSize, mapping[rightIdx])
			h := leftDst.Dy()
			if rightDst.Dy() < h {
				h = rightDst.Dy()
			}
			for y := 0; y < h; y++ {
				yy := leftDst.Min.Y + y
				l := sampleColorMappedV2(img, leftDst, leftSrc, leftDst.Max.X-1, yy)
				r := sampleColorMappedV2(img, rightDst, rightSrc, rightDst.Min.X, yy)
				rgbScore += pixelDiff(l, r)
				_, _, lb, _ := l.RGBA()
				_, _, rb, _ := r.RGBA()
				textScore += weight(yy) * float64(absIntV2(int(lb>>8)-int(rb>>8)))
			}
		}
	}
	for row := 0; row < gridSize-1; row++ {
		for col := 0; col < gridSize; col++ {
			topIdx := row*gridSize + col
			bottomIdx := (row+1)*gridSize + col
			topDst := sliderTileRect(bounds, gridSize, topIdx)
			bottomDst := sliderTileRect(bounds, gridSize, bottomIdx)
			topSrc := sliderTileRect(bounds, gridSize, mapping[topIdx])
			bottomSrc := sliderTileRect(bounds, gridSize, mapping[bottomIdx])
			w := topDst.Dx()
			if bottomDst.Dx() < w {
				w = bottomDst.Dx()
			}
			for x := 0; x < w; x++ {
				xx := topDst.Min.X + x
				t := sampleColorMappedV2(img, topDst, topSrc, xx, topDst.Max.Y-1)
				b := sampleColorMappedV2(img, bottomDst, bottomSrc, xx, bottomDst.Min.Y)
				rgbScore += pixelDiff(t, b)
				_, _, tb, _ := t.RGBA()
				_, _, bb, _ := b.RGBA()
				textScore += 0.65 * float64(absIntV2(int(tb>>8)-int(bb>>8)))
			}
		}
	}
	return rgbScore, textScore
}

func sampleColorMappedV2(img image.Image, dstRect image.Rectangle, srcRect image.Rectangle, dstX int, dstY int) color.Color {
	dx := dstRect.Dx()
	if dx < 1 {
		dx = 1
	}
	dy := dstRect.Dy()
	if dy < 1 {
		dy = 1
	}
	sx := srcRect.Min.X + (dstX-dstRect.Min.X)*srcRect.Dx()/dx
	sy := srcRect.Min.Y + (dstY-dstRect.Min.Y)*srcRect.Dy()/dy
	return img.At(sx, sy)
}

func sampleLumaMappedV2(img image.Image, dstRect image.Rectangle, srcRect image.Rectangle, dstX int, dstY int) uint8 {
	c := sampleColorMappedV2(img, dstRect, srcRect, dstX, dstY)
	r, g, b, _ := c.RGBA()
	y := (299*(r>>8) + 587*(g>>8) + 114*(b>>8)) / 1000
	return uint8(y)
}

func absFloatV2(v float64) float64 {
	if v < 0 {
		return -v
	}
	return v
}

func absIntV2(v int) int {
	if v < 0 {
		return -v
	}
	return v
}

func buildSliderCursorV2(candidateIndex int, candidateCount int) string {
	if candidateCount <= 0 {
		return "[]"
	}
	if candidateIndex < 1 {
		candidateIndex = 1
	}
	if candidateIndex > candidateCount {
		candidateIndex = candidateCount
	}

	type cursorPoint struct {
		X int `json:"x"`
		Y int `json:"y"`
	}

	startX := 570 + mathrand.Intn(40)
	startY := 875 + mathrand.Intn(30)

	denom := candidateCount - 1
	if denom < 1 {
		denom = 1
	}
	baseTargetX := 734 + (937-734)*(candidateIndex-1)/denom
	targetX := baseTargetX + mathrand.Intn(10) - 5
	targetY := 655 + mathrand.Intn(14)

	points := make([]cursorPoint, 0, 28)

	for i := 0; i < 1+mathrand.Intn(3); i++ {
		points = append(points, cursorPoint{
			X: startX + mathrand.Intn(5) - 2,
			Y: startY + mathrand.Intn(5) - 2,
		})
	}

	transitSteps := 2 + mathrand.Intn(3)
	arcOffX := mathrand.Intn(60) - 30
	arcOffY := -(mathrand.Intn(30) + 10)
	for i := 1; i <= transitSteps; i++ {
		t := float64(i) / float64(transitSteps+1)
		cx := float64(startX+targetX)/2 + float64(arcOffX)
		cy := float64(startY+targetY)/2 + float64(arcOffY)
		bx := (1-t)*(1-t)*float64(startX) + 2*t*(1-t)*cx + t*t*float64(targetX)
		by := (1-t)*(1-t)*float64(startY) + 2*t*(1-t)*cy + t*t*float64(targetY)
		jitter := int((1-t)*8) + 2
		points = append(points, cursorPoint{
			X: int(math.Round(bx)) + mathrand.Intn(jitter*2+1) - jitter,
			Y: int(math.Round(by)) + mathrand.Intn(jitter*2+1) - jitter,
		})
	}

	approachSteps := 4 + mathrand.Intn(4)
	prev := points[len(points)-1]
	for i := 1; i <= approachSteps; i++ {
		t := float64(i) / float64(approachSteps)
		ax := prev.X + int(math.Round(t*float64(targetX-prev.X))) + mathrand.Intn(5) - 2
		ay := prev.Y + int(math.Round(t*float64(targetY-prev.Y))) + mathrand.Intn(5) - 2
		points = append(points, cursorPoint{X: ax, Y: ay})
	}

	settleCount := 3 + mathrand.Intn(5)
	for i := 0; i < settleCount; i++ {
		points = append(points, cursorPoint{
			X: targetX + mathrand.Intn(7) - 3,
			Y: targetY + mathrand.Intn(7) - 3,
		})
	}

	data, err := json.Marshal(points)
	if err != nil {
		return "[]"
	}
	return string(data)
}
