package goneo

import (
	"fmt"
	"github.com/neo4j/neo4j-go-driver/v4/neo4j"
	"strings"
)

/* Instance */

type authType = int

const (
	noAuth authType = iota
	basic
)

type neo4JDriver struct {
	engine neo4j.Driver

	uri, username, password string
	realm                   string

	uniqueNodes bool
	auth        authType
}

/* Options */

type Option = func(*neo4JDriver)

func WithRealm(realm string) Option {
	return func(d *neo4JDriver) {
		d.realm = realm
	}
}

func WithUniqueNodes() Option {
	return func(d *neo4JDriver) {
		d.uniqueNodes = true
	}
}

func WithNoAuth() Option {
	return func(driver *neo4JDriver) {
		driver.auth = noAuth
	}
}

func WithBasicAuth(username, password string) Option {
	return func(driver *neo4JDriver) {
		driver.auth = basic
		driver.username, driver.password = username, password
	}
}

func NewNeo4JDriver(uri string, opts ...Option) *neo4JDriver {
	instance := &neo4JDriver{uri: uri}
	for _, opt := range opts {
		opt(instance)
	}
	return instance
}

func (d *neo4JDriver) Connect() (err error) {
	switch d.auth {
	case basic:
		d.engine, err = neo4j.NewDriver(d.uri, neo4j.BasicAuth(d.username, d.password, d.realm))
	case noAuth:
		d.engine, err = neo4j.NewDriver(d.uri, neo4j.NoAuth())
	}

	return
}

func (d *neo4JDriver) Disconnect() error {
	return d.engine.Close()
}

func (d *neo4JDriver) create(in interface{}) (*neo4j.Record, error) {
	// @TODO - Extend w/ the options pattern
	session := d.engine.NewSession(neo4j.SessionConfig{})
	defer func() { _ = session.Close() }()

	verb := "CREATE"
	if d.uniqueNodes {
		verb = "MERGE"
	}
	records, err := session.WriteTransaction(func(tx neo4j.Transaction) (interface{}, error) {
		result, err := tx.Run(fmt.Sprintf(`%s (%s:%s %s) RETURN %s`,
			verb,
			identifier(in),
			strings.Title(identifier(in)),
			marshal(in),
			returns(in)), nil)
		if err != nil {
			return nil, err
		}

		if result.Next() {
			return result.Record(), nil
		}

		return nil, result.Err()
	})
	if err != nil {
		return nil, err
	}

	return records.(*neo4j.Record), nil
}

func (d *neo4JDriver) list(identifier string) ([]*neo4j.Record, error) {
	session := d.engine.NewSession(neo4j.SessionConfig{})
	defer func() { _ = session.Close() }()

	records, err := session.ReadTransaction(func(tx neo4j.Transaction) (interface{}, error) {
		result, err := tx.Run(fmt.Sprintf("MATCH (n:%s) RETURN n", identifier), nil)
		if err != nil {
			return nil, err
		}

		var records []*neo4j.Record
		for result.Next() {
			records = append(records, result.Record())
		}

		return records, result.Err()
	})
	if err != nil {
		return nil, err
	}
	return records.([]*neo4j.Record), nil
}

func (d *neo4JDriver) link(a, b interface{}, linkName string) error {
	session := d.engine.NewSession(neo4j.SessionConfig{})
	defer func() { _ = session.Close() }()

	_, err := session.WriteTransaction(func(tx neo4j.Transaction) (interface{}, error) {
		result, err := tx.Run(fmt.Sprintf("MATCH (%s:%s), (%s:%s) WHERE %s AND %s CREATE(%s)-[:%s]->(%s)",
			identifierWithSuffix(a, "_a"), strings.Title(identifier(a)),
			identifierWithSuffix(b, "_b"), strings.Title(identifier(b)),
			filterWithSuffix(a, "_a"), filterWithSuffix(b, "_b"),
			identifierWithSuffix(a, "_a"),
			linkName,
			identifierWithSuffix(b, "_b")), nil)
		if err != nil {
			return nil, err
		}

		return nil, result.Err()
	})
	return err
}

// @TODO - Refactor all this
func (d *neo4JDriver) delete(in interface{}) error {
	session := d.engine.NewSession(neo4j.SessionConfig{})
	defer func() { _ = session.Close() }()

	_, err := session.WriteTransaction(func(tx neo4j.Transaction) (interface{}, error) {
		result, err := tx.Run(fmt.Sprintf("MATCH (%s:%s) WHERE %s DETACH DELETE(%s)",
			identifier(in), strings.Title(identifier(in)),
			filter(in),
			identifier(in)), nil)
		if err != nil {
			return nil, err
		}
		return nil, result.Err()
	})
	return err
}

func (d *neo4JDriver) find(in interface{}) ([]*neo4j.Record, error) {
	session := d.engine.NewSession(neo4j.SessionConfig{})
	defer func() { _ = session.Close() }()

	records, err := session.ReadTransaction(func(tx neo4j.Transaction) (interface{}, error) {
		result, err := tx.Run(fmt.Sprintf("MATCH (%s:%s) WHERE %s RETURN %s",
			identifier(in),
			strings.Title(identifier(in)),
			finder(in),
			identifier(in),
		), nil)
		if err != nil {
			return nil, err
		}

		var records []*neo4j.Record
		for result.Next() {
			records = append(records, result.Record())
		}

		return records, result.Err()
	})
	if err != nil {
		return nil, err
	}
	return records.([]*neo4j.Record), nil
}

func (d *neo4JDriver) getLeafs(rootType interface{}, leafType, linkName string) ([]*neo4j.Record, error) {
	session := d.engine.NewSession(neo4j.SessionConfig{})
	defer func() { _ = session.Close() }()

	rootTypeId := identifier(rootType)
	records, err := session.ReadTransaction(func(tx neo4j.Transaction) (interface{}, error) {
		result, err := tx.Run(fmt.Sprintf("MATCH (n:%s)-[:%s*]->(%s:%s) WHERE %s RETURN n",
			leafType,
			linkName,
			rootTypeId, strings.Title(rootTypeId),
			filter(rootType)), nil)
		if err != nil {
			return nil, err
		}

		var records []*neo4j.Record
		for result.Next() {
			records = append(records, result.Record())
		}

		return records, result.Err()
	})
	if err != nil {
		return nil, err
	}
	return records.([]*neo4j.Record), nil
}