package main

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
)

type direction string

const (
	dirUp   direction = "up"
	dirDown direction = "down"
)

type migrationFile struct {
	Version    uint
	Identifier string
	Direction  direction
	Filename   string // e.g. "0000001_INITIAL_CREATE.up.sql"
}

type migrationSource struct {
	dir        string
	index      []uint                                // sorted unique version numbers
	migrations map[uint]map[direction]*migrationFile // version -> direction -> file
}

// matches: 0000001_INITIAL_CREATE.up.sql
var migrationRegex = regexp.MustCompile(`^([0-9]+)_(.*)\.(up|down)\.(.*)$`)

func newMigrationSource(dir string) (*migrationSource, error) {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, fmt.Errorf("failed to read migrations directory: %w", err)
	}

	s := &migrationSource{
		dir:        dir,
		migrations: make(map[uint]map[direction]*migrationFile),
	}

	versionSet := make(map[uint]struct{})

	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		name := entry.Name()
		m := migrationRegex.FindStringSubmatch(name)
		if len(m) != 5 {
			// skip non-matching filenames (same as library)
			continue
		}

		versionUint64, err := strconv.ParseUint(m[1], 10, 64)
		if err != nil {
			continue
		}

		version := uint(versionUint64)
		dir := direction(m[3])

		mf := &migrationFile{
			Version:    version,
			Identifier: m[2],
			Direction:  dir,
			Filename:   name,
		}

		if _, ok := s.migrations[version]; !ok {
			s.migrations[version] = make(map[direction]*migrationFile)
		}

		if _, exists := s.migrations[version][dir]; exists {
			return nil, fmt.Errorf("duplicate migration version %d direction %s", version, dir)
		}

		s.migrations[version][dir] = mf
		versionSet[version] = struct{}{}
	}

	// build sorted index
	s.index = make([]uint, 0, len(versionSet))
	for v := range versionSet {
		s.index = append(s.index, v)
	}
	sort.Slice(s.index, func(i, j int) bool {
		return s.index[i] < s.index[j]
	})

	return s, nil
}

func (s *migrationSource) first() (uint, error) {
	if len(s.index) == 0 {
		return 0, os.ErrNotExist
	}
	return s.index[0], nil
}

func (s *migrationSource) next(version uint) (uint, error) {
	pos := sort.Search(len(s.index), func(i int) bool {
		return s.index[i] > version
	})
	if pos >= len(s.index) {
		return 0, os.ErrNotExist
	}
	return s.index[pos], nil
}

func (s *migrationSource) prev(version uint) (uint, error) {
	pos := sort.Search(len(s.index), func(i int) bool {
		return s.index[i] >= version
	})
	// pos is the index of version (or where it would be inserted)
	// we want the one before it
	if pos <= 0 {
		return 0, os.ErrNotExist
	}
	return s.index[pos-1], nil
}

func (s *migrationSource) readUp(version uint) (string, error) {
	if dirs, ok := s.migrations[version]; ok {
		if mf, ok := dirs[dirUp]; ok {
			return filepath.Join(s.dir, mf.Filename), nil
		}
	}
	return "", os.ErrNotExist
}

func (s *migrationSource) readDown(version uint) (string, error) {
	if dirs, ok := s.migrations[version]; ok {
		if mf, ok := dirs[dirDown]; ok {
			return filepath.Join(s.dir, mf.Filename), nil
		}
	}
	return "", os.ErrNotExist
}

func (s *migrationSource) versionExists(version uint) bool {
	pos := sort.Search(len(s.index), func(i int) bool {
		return s.index[i] >= version
	})
	return pos < len(s.index) && s.index[pos] == version
}
