package atree

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sync"

	"gordenko.dev/dima/diploma"
	"gordenko.dev/dima/diploma/atree/redo"
	"gordenko.dev/dima/diploma/bin"
)

const (
	filePerm = 0770

	// index page
	indexRecordsQtyIdx   = IndexPageSize - 7
	isDataPageNumbersIdx = IndexPageSize - 5
	indexCRC32Idx        = IndexPageSize - 4

	// data page
	timestampsSizeIdx = DataPageSize - 12
	valuesSizeIdx     = DataPageSize - 10
	prevPageIdx       = DataPageSize - 8
	dataCRC32Idx      = DataPageSize - 4

	timestampSize  = 4
	pairSize       = timestampSize + PageNoSize
	indexFooterIdx = indexRecordsQtyIdx
	dataFooterIdx  = timestampsSizeIdx

	DataPageSize  = 8192
	IndexPageSize = 1024
	PageNoSize    = 4
	//
	DataPagePayloadSize int = dataFooterIdx
)

type FreeList interface {
	ReservePage() uint32
}

type _page struct {
	PageNo         uint32
	Buf            []byte
	ReferenceCount int
}

type Atree struct {
	redoDir                string
	indexFreelist          FreeList
	dataFreelist           FreeList
	dataFile               *os.File
	indexFile              *os.File
	mutex                  sync.Mutex
	allocatedIndexPagesQty uint32
	allocatedDataPagesQty  uint32
	indexPages             map[uint32]*_page
	dataPages              map[uint32]*_page
	indexWaits             map[uint32][]chan readResult
	dataWaits              map[uint32][]chan readResult
	indexPagesToRead       []uint32
	dataPagesToRead        []uint32
	readSignalCh           chan struct{}
	writeSignalCh          chan struct{}
	writeTasksQueue        []WriteTask
}

type Options struct {
	Dir           string
	RedoDir       string
	DatabaseName  string
	DataFreeList  FreeList
	IndexFreeList FreeList
}

func New(opt Options) (*Atree, error) {
	if opt.Dir == "" {
		return nil, errors.New("Dir option is required")
	}
	if opt.RedoDir == "" {
		return nil, errors.New("RedoDir option is required")
	}
	if opt.DatabaseName == "" {
		return nil, errors.New("DatabaseName option is required")
	}
	if opt.DataFreeList == nil {
		return nil, errors.New("DataFreeList option is required")
	}
	if opt.IndexFreeList == nil {
		return nil, errors.New("IndexFreeList option is required")
	}
	// открываю или создаю dbName.data и dbName.index файлы
	var (
		indexFileName = filepath.Join(opt.Dir, opt.DatabaseName+".index")
		dataFileName  = filepath.Join(opt.Dir, opt.DatabaseName+".data")

		indexFile              *os.File
		dataFile               *os.File
		allocatedIndexPagesQty uint32
		allocatedDataPagesQty  uint32
	)

	// При создании data файла сразу создается индекс, поэтому корректное
	// состояние БД: либо оба файла есть, либо ни одного файла нет.
	isIndexExist, err := isFileExist(indexFileName)
	if err != nil {
		return nil, fmt.Errorf("check index file is exist: %s", err)
	}

	isDataExist, err := isFileExist(dataFileName)
	if err != nil {
		return nil, fmt.Errorf("check data file is exist: %s", err)
	}

	if isIndexExist {
		if isDataExist {
			// открываю оба файла
			indexFile, allocatedIndexPagesQty, err = openFile(indexFileName, IndexPageSize)
			if err != nil {
				return nil, fmt.Errorf("open index file: %s", err)
			}

			dataFile, allocatedDataPagesQty, err = openFile(dataFileName, DataPageSize)
			if err != nil {
				return nil, fmt.Errorf("open data file: %s", err)
			}
		} else {
			// нет data файла
			return nil, errors.New("not found data file")
		}
	} else {
		if isDataExist {
			// index файла нет
			return nil, errors.New("not found index file")
		} else {
			// нет обоих файлов
			indexFile, err = os.OpenFile(indexFileName, os.O_CREATE|os.O_RDWR, filePerm)
			if err != nil {
				return nil, err
			}

			dataFile, err = os.OpenFile(dataFileName, os.O_CREATE|os.O_RDWR, filePerm)
			if err != nil {
				return nil, err
			}
		}
	}

	tree := &Atree{
		redoDir:                opt.RedoDir,
		indexFreelist:          opt.IndexFreeList,
		dataFreelist:           opt.DataFreeList,
		indexFile:              indexFile,
		dataFile:               dataFile,
		allocatedIndexPagesQty: allocatedIndexPagesQty,
		allocatedDataPagesQty:  allocatedDataPagesQty,
		indexPages:             make(map[uint32]*_page),
		dataPages:              make(map[uint32]*_page),
		indexWaits:             make(map[uint32][]chan readResult),
		dataWaits:              make(map[uint32][]chan readResult),
		readSignalCh:           make(chan struct{}, 1),
		writeSignalCh:          make(chan struct{}, 1),
	}

	return tree, nil
}

func (s *Atree) Run() {
	go s.pageWriter()
	go s.pageReader()
}

// FIND

func (s *Atree) findDataPage(rootPageNo uint32, timestamp uint32) (uint32, []byte, error) {
	indexPageNo := rootPageNo
	for {
		buf, err := s.fetchIndexPage(indexPageNo)
		if err != nil {
			return 0, nil, fmt.Errorf("fetchIndexPage(%d): %s", indexPageNo, err)
		}

		foundPageNo := findPageNo(buf, timestamp)
		s.releaseIndexPage(indexPageNo)

		if buf[isDataPageNumbersIdx] == 1 {
			buf, err := s.fetchDataPage(foundPageNo)
			if err != nil {
				return 0, nil, fmt.Errorf("fetchDataPage(%d): %s", foundPageNo, err)
			}
			return foundPageNo, buf, nil
		}
		// вглубь
		indexPageNo = foundPageNo
	}
}

type pathLeg struct {
	PageNo uint32
	Data   []byte
}

type pathToDataPage struct {
	Legs       []pathLeg
	LastPageNo uint32
}

func (s *Atree) findPathToLastPage(rootPageNo uint32) (_ pathToDataPage, err error) {
	var (
		pageNo = rootPageNo
		legs   []pathLeg
	)

	for {
		var buf []byte
		buf, err = s.fetchIndexPage(pageNo)
		if err != nil {
			err = fmt.Errorf("FetchIndexPage(%d): %s", pageNo, err)
			return
		}

		legs = append(legs, pathLeg{
			PageNo: pageNo,
			Data:   buf,
			// childIdx не нужен
		})

		foundPageNo := getLastPageNo(buf)

		if buf[isDataPageNumbersIdx] == 1 {
			return pathToDataPage{
				Legs:       legs,
				LastPageNo: foundPageNo,
			}, nil
		}
		// вглубь
		pageNo = foundPageNo
	}
}

// APPEND DATA PAGE

type AppendDataPageReq struct {
	MetricID         uint32
	Timestamp        uint32
	Value            float64
	Since            uint32
	RootPageNo       uint32
	PrevPageNo       uint32
	TimestampsChunks [][]byte
	TimestampsSize   uint16
	ValuesChunks     [][]byte
	ValuesSize       uint16
}

func (s *Atree) AppendDataPage(req AppendDataPageReq) (_ redo.Report, err error) {
	var (
		flags               byte
		dataPagesToRelease  []uint32
		indexPagesToRelease []uint32
	)

	newDataPage := s.allocDataPage()
	dataPagesToRelease = append(dataPagesToRelease, newDataPage.PageNo)

	chunksToDataPage(newDataPage.Data, chunksToDataPageReq{
		PrevPageNo:       req.PrevPageNo,
		TimestampsChunks: req.TimestampsChunks,
		TimestampsSize:   req.TimestampsSize,
		ValuesChunks:     req.ValuesChunks,
		ValuesSize:       req.ValuesSize,
	})

	redoWriter, err := redo.NewWriter(redo.WriterOptions{
		Dir:              s.redoDir,
		MetricID:         req.MetricID,
		Timestamp:        req.Timestamp,
		Value:            req.Value,
		IsDataPageReused: newDataPage.IsReused,
		DataPageNo:       newDataPage.PageNo,
		Page:             newDataPage.Data,
	})
	if err != nil {
		return
	}

	if req.RootPageNo > 0 {
		var path pathToDataPage
		path, err = s.findPathToLastPage(req.RootPageNo)
		if err != nil {
			return
		}
		for _, leg := range path.Legs {
			indexPagesToRelease = append(indexPagesToRelease, leg.PageNo)
		}

		if path.LastPageNo != req.PrevPageNo {
			diploma.Abort(
				diploma.WrongPrevPageNo,
				fmt.Errorf("bug: last pageNo %d in tree != prev pageNo %d in _metric",
					path.LastPageNo, req.PrevPageNo),
			)
		}

		newPageNo := newDataPage.PageNo
		lastIdx := len(path.Legs) - 1

		for legIdx := lastIdx; legIdx >= 0; legIdx-- {
			leg := path.Legs[legIdx]

			ok := appendPair(leg.Data, req.Since, newPageNo)
			if ok {
				err = redoWriter.AppendIndexPage(leg.PageNo, leg.Data, 0)
				if err != nil {
					return
				}
				break
			}

			newIndexPage := s.allocIndexPage()
			indexPagesToRelease = append(indexPagesToRelease, newIndexPage.PageNo)
			appendPair(newIndexPage.Data, req.Since, newPageNo)
			// ставлю мітку що всі pageNo на сторінці - це data pageNo
			if legIdx == lastIdx {
				newIndexPage.Data[isDataPageNumbersIdx] = 1
			}

			flags = 0
			if newIndexPage.IsReused {
				flags |= redo.FlagReused
			}
			err = redoWriter.AppendIndexPage(newIndexPage.PageNo, newIndexPage.Data, flags)
			if err != nil {
				return
			}
			//
			newPageNo = newIndexPage.PageNo

			if legIdx == 0 {
				newRoot := s.allocIndexPage()
				indexPagesToRelease = append(indexPagesToRelease, newRoot.PageNo)
				appendPair(newRoot.Data, getSince(leg.Data), leg.PageNo) // old rootPageNo
				appendPair(newRoot.Data, req.Since, newIndexPage.PageNo)

				// Фиксирую новый root в REDO логе
				flags = redo.FlagNewRoot
				if newRoot.IsReused {
					flags |= redo.FlagReused
				}
				err = redoWriter.AppendIndexPage(newRoot.PageNo, newRoot.Data, flags)
				if err != nil {
					return
				}
				break
			}
		}
	} else {
		newRoot := s.allocIndexPage()
		indexPagesToRelease = append(indexPagesToRelease, newRoot.PageNo)
		newRoot.Data[isDataPageNumbersIdx] = 1
		appendPair(newRoot.Data, req.Since, newDataPage.PageNo)

		flags = redo.FlagNewRoot
		if newRoot.IsReused {
			flags |= redo.FlagReused
		}
		err = redoWriter.AppendIndexPage(newRoot.PageNo, newRoot.Data, flags)
		if err != nil {
			return
		}
	}

	err = redoWriter.Close()
	if err != nil {
		return
	}

	waitCh := make(chan struct{})

	task := WriteTask{
		WaitCh: waitCh,
		DataPage: redo.PageToWrite{
			PageNo: newDataPage.PageNo,
			Data:   newDataPage.Data,
		},
		IndexPages: redoWriter.IndexPagesToWrite(),
	}

	s.appendWriteTaskToQueue(task)

	<-waitCh

	for _, pageNo := range dataPagesToRelease {
		s.releaseDataPage(pageNo)
	}
	for _, pageNo := range indexPagesToRelease {
		s.releaseIndexPage(pageNo)
	}
	return redoWriter.GetReport(), nil
}

// DELETE

type PageLists struct {
	DataPages  []uint32
	IndexPages []uint32
}

type Level struct {
	PageNo   uint32
	PageData []byte
	Idx      int
	ChildQty int
}

func (s *Atree) GetAllPages(rootPageNo uint32) (_ PageLists, err error) {
	var (
		dataPages  []uint32
		indexPages []uint32
		levels     []*Level
	)

	buf, err := s.fetchIndexPage(rootPageNo)
	if err != nil {
		err = fmt.Errorf("fetchIndexPage(%d): %s", rootPageNo, err)
		return
	}
	indexPages = append(indexPages, rootPageNo)

	if buf[isDataPageNumbersIdx] == 1 {
		pageNumbers := listPageNumbers(buf)
		dataPages = append(dataPages, pageNumbers...)

		s.releaseIndexPage(rootPageNo)

		return PageLists{
			DataPages:  dataPages,
			IndexPages: indexPages,
		}, nil
	}

	levels = append(levels, &Level{
		PageNo:   rootPageNo,
		PageData: buf,
		Idx:      0,
		ChildQty: bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]),
	})

	for {
		if len(levels) == 0 {
			return PageLists{
				DataPages:  dataPages,
				IndexPages: indexPages,
			}, nil
		}

		lastIdx := len(levels) - 1
		level := levels[lastIdx]

		if level.Idx < level.ChildQty {
			pageNo := getPageNo(level.PageData, level.Idx)
			level.Idx++

			var buf []byte
			buf, err = s.fetchIndexPage(pageNo)
			if err != nil {
				err = fmt.Errorf("fetchIndexPage(%d): %s", pageNo, err)
				return
			}
			indexPages = append(indexPages, pageNo)

			if buf[isDataPageNumbersIdx] == 1 {
				pageNumbers := listPageNumbers(buf)
				dataPages = append(dataPages, pageNumbers...)

				s.releaseIndexPage(pageNo)
			} else {
				levels = append(levels, &Level{
					PageNo:   pageNo,
					PageData: buf,
					Idx:      0,
					ChildQty: bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]),
				})
			}
		} else {
			s.releaseIndexPage(level.PageNo)
			levels = levels[:lastIdx]
		}
	}
}
