// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT

package filesystem

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"code.forgejo.org/f3/gof3/v3/f3"
	"code.forgejo.org/f3/gof3/v3/id"
	"code.forgejo.org/f3/gof3/v3/kind"
	f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
	"code.forgejo.org/f3/gof3/v3/tree/generic"
	"code.forgejo.org/f3/gof3/v3/util"
)

type nodeDriver struct {
	generic.NullDriver

	content f3.Interface
}

func newNodeDriver(content f3.Interface) generic.NodeDriverInterface {
	return &nodeDriver{
		content: content.Clone(),
	}
}

func (o *nodeDriver) SetNative(any) {}

func (o *nodeDriver) GetNativeID() string {
	return o.GetNode().GetID().String()
}

func (o *nodeDriver) getBasePath() string {
	options := o.GetTreeDriver().(*treeDriver).options
	return options.Directory + o.GetNode().GetCurrentPath().String()
}

func (o *nodeDriver) getTree() generic.TreeInterface {
	return o.GetNode().GetTree()
}

func (o *nodeDriver) getF3Tree() f3_tree.TreeInterface {
	return o.getTree().(f3_tree.TreeInterface)
}

func (o *nodeDriver) isContainer() bool {
	return o.getF3Tree().IsContainer(o.getKind())
}

func (o *nodeDriver) getKind() kind.Kind {
	return o.GetNode().GetKind()
}

func (o *nodeDriver) IsNull() bool { return false }

func (o *nodeDriver) GetIDFromName(ctx context.Context, name string) id.NodeID {
	switch o.getKind() {
	case kind.KindRoot, f3_tree.KindProjects, f3_tree.KindUsers, f3_tree.KindOrganizations, f3_tree.KindRepositories:
	default:
		panic(fmt.Errorf("unxpected kind %s", o.getKind()))
	}
	for _, child := range o.GetNode().List(ctx) {
		child.Get(ctx)
		if child.ToFormat().GetName() == name {
			return child.GetID()
		}
	}
	return id.NilID
}

func (o *nodeDriver) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
	node := o.GetNode()
	node.Trace("%s '%s'", o.getKind(), node.GetID())
	children := generic.NewChildrenSlice(0)

	if o.getKind() == kind.KindRoot || page > 1 {
		return children
	}
	basePath := o.getBasePath()
	if !util.FileExists(basePath) {
		return children
	}

	f3Tree := o.getF3Tree()
	if !f3Tree.IsContainer(o.getKind()) {
		return children
	}

	node.Trace("%d '%s'", page, basePath)

	dirEntries, err := os.ReadDir(basePath)
	if err != nil {
		panic(fmt.Errorf("ReadDir %s %w", basePath, err))
	}

	for _, dirEntry := range dirEntries {
		if !strings.HasSuffix(dirEntry.Name(), ".json") {
			continue
		}
		node.Trace("  add %s", dirEntry.Name())
		child := node.CreateChild(ctx)
		i := strings.TrimSuffix(dirEntry.Name(), ".json")
		childID := id.NewNodeID(i)
		child.SetID(childID)
		children = append(children, child)
	}

	return children
}

func (o *nodeDriver) Equals(context.Context, generic.NodeInterface) bool { panic("") }

func (o *nodeDriver) LookupMappedID(id id.NodeID) id.NodeID {
	o.GetNode().Trace("%s", id)
	return id
}

func (o *nodeDriver) hasJSON() bool {
	kind := o.getKind()
	if kind == f3_tree.KindForge {
		return true
	}
	return !o.isContainer()
}

func (o *nodeDriver) Get(context.Context) bool {
	o.GetNode().Trace("'%s' '%s'", o.getKind(), o.GetNode().GetID())
	if !o.hasJSON() || o.GetNode().GetID() == id.NilID {
		return true
	}
	filename := o.getBasePath() + ".json"
	o.GetNode().Trace("'%s'", filename)
	if !util.FileExists(filename) {
		return false
	}
	f := o.NewFormat()
	loadJSON(filename, f)
	o.content = f
	o.GetNode().Trace("%s %s id=%s", o.getKind(), filename, o.content.GetID())

	idFilename := o.getBasePath() + ".id"
	if !util.FileExists(idFilename) {
		return true
	}
	mappedID, err := os.ReadFile(idFilename)
	if err != nil {
		panic(fmt.Errorf("Get %s %w", idFilename, err))
	}
	o.NullDriver.SetMappedID(id.NewNodeID(string(mappedID)))
	return true
}

func (o *nodeDriver) SetMappedID(mapped id.NodeID) {
	o.NullDriver.SetMappedID(mapped)
	o.saveMappedID()
}

func (o *nodeDriver) saveMappedID() {
	k := o.getKind()
	switch k {
	case kind.KindRoot, f3_tree.KindForge:
		return
	}
	if o.isContainer() {
		return
	}
	mappedID := o.GetMappedID()
	if mappedID == id.NilID {
		return
	}
	basePath := o.getBasePath()
	idFilename := basePath + ".id"
	o.Trace("%s", idFilename)
	if err := os.WriteFile(idFilename, []byte(o.GetMappedID().String()), 0o644); err != nil {
		panic(fmt.Errorf("%s %w", idFilename, err))
	}
}

func (o *nodeDriver) Put(ctx context.Context) id.NodeID {
	return o.upsert(ctx)
}

func (o *nodeDriver) Patch(ctx context.Context) {
	o.upsert(ctx)
}

func (o *nodeDriver) upsert(context.Context) id.NodeID {
	i := o.GetNode().GetID()
	o.GetNode().Trace("%s %s", o.getKind(), i)
	o.content.SetID(i.String())
	if !o.hasJSON() || i == id.NilID {
		return i
	}
	basePath := o.getBasePath()
	dirname := filepath.Dir(basePath)
	if !util.FileExists(dirname) {
		if err := os.MkdirAll(dirname, 0o777); err != nil {
			panic(fmt.Errorf("MakeDirAll %s %w", dirname, err))
		}
	}
	saveJSON(basePath+".json", o.content)
	o.saveMappedID()
	return i
}

func (o *nodeDriver) Delete(context.Context) {
	if o.isContainer() {
		return
	}
	basePath := o.getBasePath()
	if util.FileExists(basePath) {
		if err := os.RemoveAll(basePath); err != nil {
			panic(fmt.Errorf("RemoveAll %s %w", basePath, err))
		}
	}

	for _, ext := range []string{".id", ".json"} {
		jsonFilename := basePath + ext
		if util.FileExists(jsonFilename) {
			if err := os.Remove(jsonFilename); err != nil {
				panic(fmt.Errorf("RemoveAll %s %w", basePath, err))
			}
		}
	}
	o.content = o.NewFormat()
}

func (o *nodeDriver) NewFormat() f3.Interface {
	return o.getTree().(f3_tree.TreeInterface).NewFormat(o.getKind())
}

func (o *nodeDriver) FromFormat(content f3.Interface) {
	o.content = content
}

func (o *nodeDriver) ToFormat() f3.Interface {
	return o.content.Clone()
}

func (o *nodeDriver) String() string {
	return o.content.GetID()
}
