package torrent

import (
	"context"
	"sync"
)

type pieceTask struct {
	Index int
}

type assignPieceRequest struct {
	have       []bool
	hasInfo    bool
	responseCh chan assignPieceResponse
}

type assignPieceResponse struct {
	task pieceTask
	ok   bool
}

type reportPieceRequest struct {
	pieceIndex int
	success    bool
	responseCh chan bool
}

type pieceScheduler struct {
	assignCh   chan assignPieceRequest
	reportCh   chan reportPieceRequest
	progressCh chan int
	doneCh     chan struct{}
	stopCh     chan struct{}
	stopOnce   sync.Once
}

type pieceState uint8

const (
	piecePending pieceState = iota
	pieceInProgress
	pieceDone
)

func newPieceScheduler(pieceCount int) *pieceScheduler {
	ps := &pieceScheduler{
		assignCh:   make(chan assignPieceRequest, 128),
		reportCh:   make(chan reportPieceRequest, 128),
		progressCh: make(chan int, 128),
		doneCh:     make(chan struct{}),
		stopCh:     make(chan struct{}),
	}

	go ps.run(pieceCount)
	return ps
}

func (ps *pieceScheduler) Acquire(ctx context.Context, have []bool, hasInfo bool) (pieceTask, bool, error) {
	responseCh := make(chan assignPieceResponse, 1)
	req := assignPieceRequest{
		have:       have,
		hasInfo:    hasInfo,
		responseCh: responseCh,
	}

	select {
	case <-ctx.Done():
		return pieceTask{}, false, ctx.Err()
	case <-ps.doneCh:
		return pieceTask{}, false, nil
	case ps.assignCh <- req:
	}

	select {
	case <-ctx.Done():
		return pieceTask{}, false, ctx.Err()
	case <-ps.doneCh:
		return pieceTask{}, false, nil
	case response := <-responseCh:
		return response.task, response.ok, nil
	}
}

func (ps *pieceScheduler) Report(ctx context.Context, pieceIndex int, success bool) (bool, error) {
	responseCh := make(chan bool, 1)
	req := reportPieceRequest{
		pieceIndex: pieceIndex,
		success:    success,
		responseCh: responseCh,
	}

	select {
	case <-ctx.Done():
		return false, ctx.Err()
	case <-ps.doneCh:
		return false, nil
	case ps.reportCh <- req:
	}

	select {
	case <-ctx.Done():
		return false, ctx.Err()
	case <-ps.doneCh:
		return false, nil
	case accepted := <-responseCh:
		return accepted, nil
	}
}

func (ps *pieceScheduler) Progress() <-chan int {
	return ps.progressCh
}

func (ps *pieceScheduler) Done() <-chan struct{} {
	return ps.doneCh
}

func (ps *pieceScheduler) Stop() {
	ps.stopOnce.Do(func() {
		close(ps.stopCh)
	})
}

func (ps *pieceScheduler) run(pieceCount int) {
	states := make([]pieceState, pieceCount)
	completed := 0
	finish := func() {
		close(ps.doneCh)
		close(ps.progressCh)
	}

	if pieceCount == 0 {
		finish()
		return
	}

	for {
		if completed >= pieceCount {
			finish()
			return
		}

		select {
		case <-ps.stopCh:
			finish()
			return
		case req := <-ps.assignCh:
			pieceIndex := selectPendingPiece(states, req.have, req.hasInfo)
			if pieceIndex >= 0 {
				states[pieceIndex] = pieceInProgress
				req.responseCh <- assignPieceResponse{
					task: pieceTask{Index: pieceIndex},
					ok:   true,
				}
				continue
			}
			req.responseCh <- assignPieceResponse{ok: false}
		case req := <-ps.reportCh:
			if req.pieceIndex < 0 || req.pieceIndex >= len(states) {
				req.responseCh <- false
				continue
			}

			if req.success {
				if states[req.pieceIndex] == pieceDone {
					req.responseCh <- false
					continue
				}
				states[req.pieceIndex] = pieceDone
				completed++
				select {
				case ps.progressCh <- req.pieceIndex:
				default:
				}
				req.responseCh <- true
				continue
			}

			if states[req.pieceIndex] == pieceInProgress {
				states[req.pieceIndex] = piecePending
			}
			req.responseCh <- false
		}
	}
}

func selectPendingPiece(states []pieceState, have []bool, hasInfo bool) int {
	firstPending := firstPendingPiece(states)
	if firstPending < 0 {
		return -1
	}

	// If peer has not sent bitfield/have yet, treat it as potentially having any piece.
	if !hasInfo {
		return firstPending
	}

	// Prefer pieces explicitly advertised by peer.
	for pieceIndex, state := range states {
		if state != piecePending {
			continue
		}
		if pieceIndex < len(have) && have[pieceIndex] {
			return pieceIndex
		}
	}

	// Some peers send missing/truncated availability info; allow optimistic probing.
	if len(have) == 0 || len(have) < len(states) {
		return firstPending
	}

	return -1
}

func firstPendingPiece(states []pieceState) int {
	for pieceIndex, state := range states {
		if state == piecePending {
			return pieceIndex
		}
	}
	return -1
}
