package project

import (
	"context"
	"fmt"
	"iter"
	"sync"

	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
)

type checkerPool struct {
	maxCheckers int
	program     *compiler.Program

	mu                  sync.Mutex
	cond                *sync.Cond
	createCheckersOnce  sync.Once
	checkers            []*checker.Checker
	inUse               map[*checker.Checker]bool
	fileAssociations    map[*ast.SourceFile]int
	requestAssociations map[string]int
	log                 func(msg string)
}

var _ compiler.CheckerPool = (*checkerPool)(nil)

func newCheckerPool(maxCheckers int, program *compiler.Program, log func(msg string)) *checkerPool {
	pool := &checkerPool{
		program:             program,
		maxCheckers:         maxCheckers,
		checkers:            make([]*checker.Checker, maxCheckers),
		inUse:               make(map[*checker.Checker]bool),
		requestAssociations: make(map[string]int),
		log:                 log,
	}

	pool.cond = sync.NewCond(&pool.mu)
	return pool
}

func (p *checkerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
	p.mu.Lock()
	defer p.mu.Unlock()

	requestID := core.GetRequestID(ctx)
	if requestID != "" {
		if checker, release := p.getRequestCheckerLocked(requestID); checker != nil {
			return checker, release
		}
	}

	if p.fileAssociations == nil {
		p.fileAssociations = make(map[*ast.SourceFile]int)
	}

	if index, ok := p.fileAssociations[file]; ok {
		checker := p.checkers[index]
		if checker != nil {
			if inUse := p.inUse[checker]; !inUse {
				p.inUse[checker] = true
				if requestID != "" {
					p.requestAssociations[requestID] = index
				}
				return checker, p.createRelease(requestID, index, checker)
			}
		}
	}

	checker, index := p.getCheckerLocked(requestID)
	p.fileAssociations[file] = index
	return checker, p.createRelease(requestID, index, checker)
}

func (p *checkerPool) GetChecker(ctx context.Context) (*checker.Checker, func()) {
	p.mu.Lock()
	defer p.mu.Unlock()
	checker, index := p.getCheckerLocked(core.GetRequestID(ctx))
	return checker, p.createRelease(core.GetRequestID(ctx), index, checker)
}

func (p *checkerPool) Files(checker *checker.Checker) iter.Seq[*ast.SourceFile] {
	panic("unimplemented")
}

func (p *checkerPool) GetAllCheckers(ctx context.Context) ([]*checker.Checker, func()) {
	p.mu.Lock()
	defer p.mu.Unlock()

	requestID := core.GetRequestID(ctx)
	if requestID == "" {
		panic("cannot call GetAllCheckers on a project.checkerPool without a request ID")
	}

	// A request can only access one checker
	if c, release := p.getRequestCheckerLocked(requestID); c != nil {
		return []*checker.Checker{c}, release
	}

	c, release := p.GetChecker(ctx)
	return []*checker.Checker{c}, release
}

func (p *checkerPool) getCheckerLocked(requestID string) (*checker.Checker, int) {
	if checker, index := p.getImmediatelyAvailableChecker(); checker != nil {
		p.inUse[checker] = true
		if requestID != "" {
			p.requestAssociations[requestID] = index
		}
		return checker, index
	}

	if !p.isFullLocked() {
		checker, index := p.createCheckerLocked()
		p.inUse[checker] = true
		if requestID != "" {
			p.requestAssociations[requestID] = index
		}
		return checker, index
	}

	checker, index := p.waitForAvailableChecker()
	p.inUse[checker] = true
	if requestID != "" {
		p.requestAssociations[requestID] = index
	}
	return checker, index
}

func (p *checkerPool) getRequestCheckerLocked(requestID string) (*checker.Checker, func()) {
	if index, ok := p.requestAssociations[requestID]; ok {
		checker := p.checkers[index]
		if checker != nil {
			if inUse := p.inUse[checker]; !inUse {
				p.inUse[checker] = true
				return checker, p.createRelease(requestID, index, checker)
			}
			// Checker is in use, but by the same request - assume it's the
			// same goroutine or is managing its own synchronization
			return checker, noop
		}
	}
	return nil, noop
}

func (p *checkerPool) getImmediatelyAvailableChecker() (*checker.Checker, int) {
	for i, checker := range p.checkers {
		if checker == nil {
			continue
		}
		if inUse := p.inUse[checker]; !inUse {
			return checker, i
		}
	}

	return nil, -1
}

func (p *checkerPool) waitForAvailableChecker() (*checker.Checker, int) {
	p.log("checkerpool: Waiting for an available checker")
	for {
		p.cond.Wait()
		checker, index := p.getImmediatelyAvailableChecker()
		if checker != nil {
			return checker, index
		}
	}
}

func (p *checkerPool) createRelease(requestId string, index int, checker *checker.Checker) func() {
	return func() {
		p.mu.Lock()
		defer p.mu.Unlock()

		delete(p.requestAssociations, requestId)
		if checker.WasCanceled() {
			// Canceled checkers must be disposed
			p.log(fmt.Sprintf("checkerpool: Checker for request %s was canceled, disposing it", requestId))
			p.checkers[index] = nil
			delete(p.inUse, checker)
		} else {
			p.inUse[checker] = false
		}
		p.cond.Signal()
	}
}

func (p *checkerPool) isFullLocked() bool {
	for _, checker := range p.checkers {
		if checker == nil {
			return false
		}
	}
	return true
}

func (p *checkerPool) createCheckerLocked() (*checker.Checker, int) {
	for i, existing := range p.checkers {
		if existing == nil {
			checker := checker.NewChecker(p.program)
			p.checkers[i] = checker
			return checker, i
		}
	}
	panic("called createCheckerLocked when pool is full")
}

func (p *checkerPool) isRequestCheckerInUse(requestID string) bool {
	p.mu.Lock()
	defer p.mu.Unlock()

	if index, ok := p.requestAssociations[requestID]; ok {
		checker := p.checkers[index]
		if checker != nil {
			return p.inUse[checker]
		}
	}
	return false
}

func (p *checkerPool) size() int {
	p.mu.Lock()
	defer p.mu.Unlock()
	size := 0
	for _, checker := range p.checkers {
		if checker != nil {
			size++
		}
	}
	return size
}

func noop() {}
