package main

import (
	// External
	werr "git.gibonuddevalla.se/go/wrappederror"
	"github.com/derektata/lorem/ipsum"
	"github.com/jmoiron/sqlx"

	// Standard
	"database/sql"
	"fmt"
	"time"
)

type ChecklistItem struct {
	ID      int
	GroupID int `db:"checklist_group_id"`
	Order   int
	Label   string
	Checked bool
}

type ChecklistGroup struct {
	ID     int
	NodeID int `db:"node_id"`
	Order  int
	Label  string
	Items  []ChecklistItem
}

type TreeNode struct {
	UUID       string
	ParentUUID string `db:"parent_uuid"`
	Name       string
	Created    time.Time
	Updated    time.Time
	Deleted    bool
	CreatedSeq uint64        `db:"created_seq"`
	UpdatedSeq uint64        `db:"updated_seq"`
	DeletedSeq sql.NullInt64 `db:"deleted_seq"`
}

type Node struct {
	UUID             string
	UserID           int    `db:"user_id"`
	ParentUUID       string `db:"parent_uuid"`
	Name             string
	Created          time.Time
	Updated          time.Time
	Deleted          bool
	CreatedSeq       uint64        `db:"created_seq"`
	UpdatedSeq       uint64        `db:"updated_seq"`
	DeletedSeq       sql.NullInt64 `db:"deleted_seq"`
	Content          string
	ContentEncrypted string `db:"content_encrypted" json:"-"`
	Markdown         bool

	// CryptoKeyID    int    `db:"crypto_key_id"`
	//Files           []File
	//ChecklistGroups []ChecklistGroup
}

func NodeTree(userID, offset int, synced uint64) (nodes []TreeNode, maxSeq uint64, moreRowsExist bool, err error) { // {{{
	const LIMIT = 100
	var rows *sqlx.Rows
	rows, err = db.Queryx(`
		SELECT
			uuid,
			COALESCE(parent_uuid, '') AS parent_uuid,
			name,
			created,
			updated,
			deleted IS NOT NULL AS deleted,
			created_seq,
			updated_seq,
			deleted_seq
		FROM
			public.node
		WHERE
			user_id = $1 AND 
			NOT history AND (
				created_seq > $4 OR
				updated_seq > $4 OR
				deleted_seq > $4
			)
		ORDER BY
			created ASC
		LIMIT $2 OFFSET $3
		`,
		userID,
		LIMIT+1,
		offset,
		synced,
	)
	if err != nil {
		return
	}
	defer rows.Close()

	nodes = []TreeNode{}
	numNodes := 0
	for rows.Next() {
		// Query selects up to one more row than the decided limit.
		// Saves one SQL query for row counting.
		// Thus if numNodes is larger than the limit, more rows exist for the next call.
		numNodes++
		if numNodes > LIMIT {
			moreRowsExist = true
			return
		}

		node := TreeNode{}
		if err = rows.StructScan(&node); err != nil {
			return
		}
		nodes = append(nodes, node)

		// DeletedSeq will be 0 if invalid, and thus not be a problem for the max function.
		maxSeq = max(maxSeq, node.CreatedSeq, node.UpdatedSeq, uint64(node.DeletedSeq.Int64))
	}

	return
} // }}}
func Nodes(userID, offset int, synced uint64, clientUUID string) (nodes []Node, maxSeq uint64, moreRowsExist bool, err error) { // {{{
	var rows *sqlx.Rows
	rows, err = db.Queryx(`
		SELECT
			uuid,
			COALESCE(parent_uuid, '') AS parent_uuid,
			name,
			created,
			updated,
			deleted IS NOT NULL AS deleted,
			created_seq,
			updated_seq,
			deleted_seq,
			content,
			content_encrypted,
			markdown
		FROM
			public.node
		WHERE
			user_id = $1 AND
			client != $5 AND
			NOT history AND (
				created_seq > $4 OR
				updated_seq > $4 OR
				deleted_seq > $4
			)
		ORDER BY
			id ASC
		LIMIT $2 OFFSET $3
		`,
		userID,
		SYNC_PAGINATION+1,
		offset,
		synced,
		clientUUID,
	)
	if err != nil {
		return
	}
	defer rows.Close()

	nodes = []Node{}
	numNodes := 0
	for rows.Next() {
		// Query selects up to one more row than the decided limit.
		// Saves one SQL query for row counting.
		// Thus if numNodes is larger than the limit, more rows exist for the next call.
		numNodes++
		if numNodes > SYNC_PAGINATION {
			moreRowsExist = true
			return
		}

		node := Node{}
		if err = rows.StructScan(&node); err != nil {
			return
		}
		nodes = append(nodes, node)

		// DeletedSeq will be 0 if invalid, and thus not be a problem for the max function.
		maxSeq = max(maxSeq, node.CreatedSeq, node.UpdatedSeq, uint64(node.DeletedSeq.Int64))
	}

	return
} // }}}
func NodesCount(userID int, synced uint64, clientUUID string) (count int, err error) { // {{{
	row := db.QueryRow(`
		SELECT
			COUNT(*)
		FROM
			public.node
		WHERE
			user_id = $1 AND
			client != $3 AND
			NOT history AND (
				created_seq > $2 OR
				updated_seq > $2 OR
				deleted_seq > $2
			)
		`,
		userID,
		synced,
		clientUUID,
	)
	err = row.Scan(&count)
	if err != nil {
		err = werr.Wrap(err).WithData(
			struct {
				UserID     int
				Synced     uint64
				ClientUUID string
			}{
				userID, synced, clientUUID,
			},
		)
	}
	return
} // }}}
func RetrieveNode(userID int, nodeUUID string) (node Node, err error) { // {{{
	var rows *sqlx.Row
	rows = db.QueryRowx(`
		SELECT
			uuid,
			user_id,
			COALESCE(parent_uuid, '') AS parent_uuid,
			/*COALESCE(crypto_key_id, 0) AS crypto_key_id,*/
			name,
			content,
			content_encrypted,
			markdown,
			0 AS level
		FROM node
		WHERE
			user_id = $1 AND
			uuid = $2
		
		`,
		userID,
		nodeUUID,
	)
	node = Node{}
	if err = rows.StructScan(&node); err != nil {
		return
	}

	return
} // }}}
func RetrieveNodeHistory(userID int, nodeUUID string, offset int) (nodes []Node, hasMore bool, err error) { // {{{
	nodes = []Node{}

	var rows *sqlx.Rows
	rows, err = db.Queryx(`
		SELECT
			uuid,
			user_id,
			name,
			created,
			updated,
			content,
			content_encrypted
		FROM node_history
		WHERE
			user_id = $1 AND
			uuid = $2
		LIMIT $3 OFFSET $4
		`,
		userID,
		nodeUUID,
		SYNC_PAGINATION+1,
		offset,
	)
	if err != nil {
		err = werr.Wrap(err)
		return
	}
	defer rows.Close()

	for rows.Next() {
		node := Node{}
		if err = rows.StructScan(&node); err != nil {
			err = werr.Wrap(err)
			return
		}
		nodes = append(nodes, node)
	}

	if len(nodes) > SYNC_PAGINATION {
		hasMore = true
		nodes = nodes[0 : len(nodes)-1]
	}
	return
} // }}}
func RetrieveNodeHistoryCount(userID int, nodeUUID string) (count int, err error) { // {{{
	var row *sql.Row
	row = db.QueryRow(`
		SELECT
			COUNT(*)
		FROM node_history
		WHERE
			user_id = $1 AND
			uuid = $2
		`,
		userID,
		nodeUUID,
	)
	if err = row.Scan(&count); err != nil {
		err = werr.Wrap(err)
		return
	}

	return
} // }}}
func NodeCrumbs(nodeUUID string) (nodes []Node, err error) { // {{{
	var rows *sqlx.Rows
	rows, err = db.Queryx(`
		WITH RECURSIVE nodes AS (
			SELECT
				uuid,
				COALESCE(parent_uuid, '') AS parent_uuid,
				name
			FROM node
			WHERE
				uuid = $1

			UNION
			
			SELECT
				n.uuid,
				COALESCE(n.parent_uuid, '') AS parent_uuid,
				n.name
			FROM node n
			INNER JOIN nodes nr ON n.uuid = nr.parent_uuid
		)
		SELECT * FROM nodes
	`, nodeUUID)
	if err != nil {
		return
	}
	defer rows.Close()

	nodes = []Node{}
	for rows.Next() {
		node := Node{}
		if err = rows.StructScan(&node); err != nil {
			return
		}
		nodes = append(nodes, node)
	}
	return
} // }}}

func LoremIpsum() (err error) {
	var numNodesRow *sql.Row
	var numNodes int
	numNodesRow = db.QueryRow(`SELECT COUNT(id) FROM node WHERE name LIKE 'G-%'`)
	err = numNodesRow.Scan(&numNodes)
	if err != nil {
		return
	}

	var rows *sql.Rows
	rows, err = db.Query(`SELECT id FROM node WHERE name LIKE 'G-%'`)
	if err != nil {
		return
	}
	defer rows.Close()

	lipsum := lorem.NewGenerator()
	var id, counter int
	for rows.Next() {
		counter++
		if counter&100 == 0 {
			fmt.Printf("%04d\n", counter)
		}
		err = rows.Scan(&id)
		if err != nil {
			return
		}

		db.Exec(`UPDATE node SET content=$2 WHERE id=$1`, id, lipsum.GenerateParagraphs(2))
	}
	fmt.Printf("%04d\n", counter)
	return
}

func TestData() (err error) {
	for range 8 {
		hash1, name1, _ := generateOneTestNode("", "G")
		for range 8 {
			hash2, name2, _ := generateOneTestNode(hash1, name1)
			for range 8 {
				hash3, name3, _ := generateOneTestNode(hash2, name2)
				for range 8 {
					generateOneTestNode(hash3, name3)
				}
			}

		}
	}
	return
}

func generateOneTestNode(parentUUID, parentPath string) (hash, name string, err error) {
	var sqlParentUUID sql.NullString
	if parentUUID != "" {
		sqlParentUUID.String = parentUUID
		sqlParentUUID.Valid = true
	}
	query := `
		INSERT INTO node(user_id, parent_uuid, name)
		VALUES(
			1,
			$1,
			CONCAT(
				$2::text,
				'-',
				LPAD(nextval('test_data')::text, 4, '0')
			)
		)
		RETURNING uuid, name`

	var row *sql.Row
	row = db.QueryRow(query, sqlParentUUID, parentPath)
	err = row.Scan(&hash, &name)
	if err != nil {
		Log.Error("generate", "error", err)
		return
	}

	return
}
