package guts_cli

// Low level access to pages / data-structures of the bbolt file.

import (
	"errors"
	"fmt"
	"io"
	"os"

	"go.etcd.io/bbolt/internal/common"
)

var (
	// ErrCorrupt is returned when a checking a data file finds errors.
	ErrCorrupt = errors.New("invalid value")
)

// ReadPage reads Page info & full Page data from a path.
// This is not transactionally safe.
func ReadPage(path string, pageID uint64) (*common.Page, []byte, error) {
	// Find Page size.
	pageSize, hwm, err := ReadPageAndHWMSize(path)
	if err != nil {
		return nil, nil, fmt.Errorf("read Page size: %s", err)
	}

	// Open database file.
	f, err := os.Open(path)
	if err != nil {
		return nil, nil, err
	}
	defer f.Close()

	// Read one block into buffer.
	buf := make([]byte, pageSize)
	if n, err := f.ReadAt(buf, int64(pageID*pageSize)); err != nil {
		return nil, nil, err
	} else if n != len(buf) {
		return nil, nil, io.ErrUnexpectedEOF
	}

	// Determine total number of blocks.
	p := common.LoadPage(buf)
	if p.Id() != common.Pgid(pageID) {
		return nil, nil, fmt.Errorf("error: %w due to unexpected Page id: %d != %d", ErrCorrupt, p.Id(), pageID)
	}
	overflowN := p.Overflow()
	if overflowN >= uint32(hwm)-3 { // we exclude 2 Meta pages and the current Page.
		return nil, nil, fmt.Errorf("error: %w, Page claims to have %d overflow pages (>=hwm=%d). Interrupting to avoid risky OOM", ErrCorrupt, overflowN, hwm)
	}

	if overflowN == 0 {
		return p, buf, nil
	}

	// Re-read entire Page (with overflow) into buffer.
	buf = make([]byte, (uint64(overflowN)+1)*pageSize)
	if n, err := f.ReadAt(buf, int64(pageID*pageSize)); err != nil {
		return nil, nil, err
	} else if n != len(buf) {
		return nil, nil, io.ErrUnexpectedEOF
	}
	p = common.LoadPage(buf)
	if p.Id() != common.Pgid(pageID) {
		return nil, nil, fmt.Errorf("error: %w due to unexpected Page id: %d != %d", ErrCorrupt, p.Id(), pageID)
	}

	return p, buf, nil
}

func WritePage(path string, pageBuf []byte) error {
	page := common.LoadPage(pageBuf)
	pageSize, _, err := ReadPageAndHWMSize(path)
	if err != nil {
		return err
	}
	expectedLen := pageSize * (uint64(page.Overflow()) + 1)
	if expectedLen != uint64(len(pageBuf)) {
		return fmt.Errorf("WritePage: len(buf):%d != pageSize*(overflow+1):%d", len(pageBuf), expectedLen)
	}
	f, err := os.OpenFile(path, os.O_WRONLY, 0)
	if err != nil {
		return err
	}
	defer f.Close()
	_, err = f.WriteAt(pageBuf, int64(page.Id())*int64(pageSize))
	return err
}

// ReadPageAndHWMSize reads Page size and HWM (id of the last+1 Page).
// This is not transactionally safe.
func ReadPageAndHWMSize(path string) (uint64, common.Pgid, error) {
	// Open database file.
	f, err := os.Open(path)
	if err != nil {
		return 0, 0, err
	}
	defer f.Close()

	// Read 4KB chunk.
	buf := make([]byte, 4096)
	if _, err := io.ReadFull(f, buf); err != nil {
		return 0, 0, err
	}

	// Read Page size from metadata.
	m := common.LoadPageMeta(buf)
	if m.Magic() != common.Magic {
		return 0, 0, fmt.Errorf("the Meta Page has wrong (unexpected) magic")
	}
	return uint64(m.PageSize()), common.Pgid(m.Pgid()), nil
}

// GetRootPage returns the root-page (according to the most recent transaction).
func GetRootPage(path string) (root common.Pgid, activeMeta common.Pgid, err error) {
	m, id, err := GetActiveMetaPage(path)
	if err != nil {
		return 0, id, err
	}
	return m.RootBucket().RootPage(), id, nil
}

// GetActiveMetaPage returns the active meta page and its page ID (0 or 1).
func GetActiveMetaPage(path string) (*common.Meta, common.Pgid, error) {
	_, buf0, err0 := ReadPage(path, 0)
	if err0 != nil {
		return nil, 0, err0
	}
	m0 := common.LoadPageMeta(buf0)
	_, buf1, err1 := ReadPage(path, 1)
	if err1 != nil {
		return nil, 1, err1
	}
	m1 := common.LoadPageMeta(buf1)
	if m0.Txid() < m1.Txid() {
		return m1, 1, nil
	} else {
		return m0, 0, nil
	}
}
