package database

import (
	"log"

	"github.com/neo4j/neo4j-go-driver/neo4j"
)

//Database
type Database struct {
	driver  neo4j.Driver
	session neo4j.Session
}

//NewDatabaseClient creates a database client
func NewDatabaseClient(uri, username, password string, encrypted bool) Database {
	//TODO: defer close()?
	//		probably pretty nasty since it creates copies
	driver := createDriver(uri, username, password, encrypted)
	session := createSession(driver)

	return Database{
		driver:  driver,
		session: session,
	}

}

//createDriver creates a neo4j.Driver instance
func createDriver(uri, username, password string, encrypted bool) neo4j.Driver {
	driver, err := neo4j.NewDriver(
		uri,
		neo4j.BasicAuth(username, password, ""),
		func(c *neo4j.Config) {
			c.Encrypted = encrypted
		},
	)

	logError("failed creating database Driver", err)

	return driver
}

//createSession creates a neo4j.Session
func createSession(driver neo4j.Driver) neo4j.Session {
	sessionConfig := neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}
	session, err := driver.NewSession(sessionConfig)

	logError("failed creating database session", err)

	return session
}

//StoreNodes stores the given nodes to the database
func (d Database) StoreNodes(json string) {
	session := d.session

	query :=
		`
		WITH apoc.convert.fromJsonMap($stringToAdd)
		AS value
		UNWIND value.data as d
		MERGE (device:Device {id: d.object_id})
		ON CREATE SET device.nativeName = d.object_data.name[0].value,
			device.deviceType = d.object_data.name[1].value,
			device.serialNumber = d.object_data.name[2].value,
			device.softwareVersion = d.object_data.name[3].value,
			device.` + "`operational-state` = d.object_data.`operational-state`"

	_, err := session.Run(
		query, map[string]interface{}{
			"stringToAdd": json,
		})

	logError("failed storing Nodes into database", err)

	log.Printf("successfully added Nodes into database")
}

//StoreNodeEdgePoints stores the given node edge points (interfaces)
func (d Database) StoreNodeEdgePoints(json string) {
	session := d.session

	queryEdgePoints :=
		`
		WITH apoc.convert.fromJsonMap($stringToAdd)
		AS value
		UNWIND value.data as i
		MERGE (interface:Interface {id: i.object_id})
		ON CREATE SET interface.object_type =i.object_type,
		interface.localId = i.object_data.name[0].value,
		interface.location = i.object_data.name[1].value,
		interface.` + "`containing-node` = i.object_data.`containing-node`"

	_, err := session.Run(
		queryEdgePoints, map[string]interface{}{
			"stringToAdd": json,
		})

	logError("failed storing NodeEdgePoints into database", err)

	setNodeNodeEdgePointsRelation(session)

	log.Printf("successfully added NodeEdgePoints into database")

}

//setNodeNodeEdgePointsRelation creates the database relations between "nodes" and "node edge points"
func setNodeNodeEdgePointsRelation(session neo4j.Session) {
	query :=
		`
		MATCH (d:Device), (i:Interface)
		WHERE d.id = i.` + "`containing-node`" + `
		CREATE (i)-[:BELONGS_TO]->(d)
		`

	_, err := session.Run(
		query, map[string]interface{}{})

	logError("failed storing NodeNodeEdgePointsRelation into database", err)

	log.Printf("successfully stored NodeNodeEdgePointsRelation into database")
}

//logError logs error with custom and error message
func logError(message string, err error) {
	if err != nil {
		log.Fatalf("%v: %v", message, err)
	}
}