package main

import (
	// External
	werr "git.ahall.se/go/wrappederror"
	"github.com/expr-lang/expr"
	"github.com/expr-lang/expr/ast"
	"github.com/expr-lang/expr/parser"
	"github.com/jackc/pgx/v5/pgconn"

	// Standard
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
)

type Trigger struct {
	ID              int
	Name            string
	SectionID       int `db:"section_id"`
	Expression      string
	Datapoints      []string
	DatapointValues map[string]any
}

type ExprRenamePatcher struct {
	OldName string
	NewName string
}

func (p ExprRenamePatcher) Visit(node *ast.Node) {
	if n, ok := (*node).(*ast.IdentifierNode); ok && n.Value == p.OldName {
		ast.Patch(node, &ast.IdentifierNode{Value: p.NewName})
	}
}

func TriggerCreate(sectionID int, name string) (t Trigger, err error) { // {{{
	t.SectionID = sectionID
	t.Name = name
	t.Expression = "false"
	err = t.Update()
	return
} // }}}
func TriggersRetrieve() (areas []Area, err error) { // {{{
	areas = []Area{}

	row := db.QueryRow(
		context.Background(),
		`
		WITH section_triggers AS (
			SELECT
				s.id AS id,
				s.area_id,
				s.name AS name,
				jsonb_agg(
					to_jsonb(t.*)
				) AS triggers
			FROM section s
			LEFT JOIN "trigger" t ON t.section_id = s.id
			GROUP BY
				s.id, s.name)
				
		SELECT
			 jsonb_agg(jsonsections)
		FROM (
			SELECT
				a.id,
				a.name,
				jsonb_agg(
					to_jsonb(
						s.*
					)
				) AS sections
			FROM area a
			LEFT JOIN section_triggers s ON s.area_id = a.id
			GROUP BY
				a.id, a.name
		) jsonsections
		`,
	)

	var jsonData []byte
	err = row.Scan(&jsonData)
	if err != nil {
		err = werr.Wrap(err)
		return
	}

	if jsonData == nil {
		return
	}

	err = json.Unmarshal(jsonData, &areas)
	if err != nil {
		err = werr.Wrap(err)
		return
	}

	return
} // }}}
func TriggersRetrieveByDatapoint(datapointName string) (triggers []Trigger, err error) { // {{{
	triggers = []Trigger{}
	row := db.QueryRow(
		context.Background(),
		`
		SELECT jsonb_agg(t.*)
		FROM public."trigger" t
		WHERE
			datapoints @> $1
		`,
		fmt.Sprintf(`["%s"]`, datapointName),
	)

	var data []byte
	err = row.Scan(&data)
	if err != nil {
		err = werr.Wrap(err).WithData(datapointName)
		return
	}

	// no triggers found for this datapoint.
	if data == nil {
		return
	}

	err = json.Unmarshal(data, &triggers)
	if err != nil {
		err = werr.Wrap(err).WithData(datapointName)
		return
	}

	return
} // }}}
func TriggerRetrieve(id int) (trigger Trigger, err error) { // {{{
	row := db.QueryRow(context.Background(), `SELECT to_jsonb(t.*) FROM "trigger" t WHERE id=$1`, id)
	var jsonData []byte
	err = row.Scan(&jsonData)
	if err != nil {
		err = werr.Wrap(err)
		return
	}

	err = json.Unmarshal(jsonData, &trigger)
	return
} // }}}
func TriggerDelete(id int) (err error) { // {{{
	_, err = db.Exec(context.Background(), `DELETE FROM public.trigger WHERE id=$1`, id)
	if err != nil {
		return werr.Wrap(err).WithData(id)
	}
	return
} // }}}

func (t *Trigger) Validate() (ok bool, err error) { // {{{
	if strings.TrimSpace(t.Name) == "" {
		err = fmt.Errorf("Name can't be empty")
		return
	}

	if strings.TrimSpace(t.Expression) == "" {
		err = fmt.Errorf("Expression can't be empty")
		return
	}

	return true, nil
} // }}}
func (t *Trigger) Update() (err error) { // {{{
	var ok bool
	if ok, err = t.Validate(); !ok {
		return
	}

	if t.Datapoints == nil {
		t.Datapoints = []string{}
	}
	jsonDatapoints, _ := json.Marshal(t.Datapoints)
	if t.ID == 0 {
		row := db.QueryRow(
			context.Background(),
			`
			INSERT INTO "trigger"(name, section_id, expression, datapoints)
			VALUES($1, $2, $3, $4)
			RETURNING id
			`,
			t.Name,
			t.SectionID,
			t.Expression,
			jsonDatapoints,
		)
		err = row.Scan(&t.ID)
		if err != nil {
			err = werr.Wrap(err).WithData(
				struct {
					SectionID      int
					Name           string
					Expression     string
					JsonDataPoints []byte
				}{
					t.SectionID,
					t.Name,
					t.Expression,
					jsonDatapoints,
				},
			)
			return
		}
	} else {
		_, err = db.Exec(
			context.Background(),
			`
			UPDATE "trigger"
			SET
				name=$2,
				expression=$3,
				datapoints=$4
			WHERE
				id=$1
			`,
			t.ID,
			t.Name,
			t.Expression,
			jsonDatapoints,
		)
	}

	var pgErr *pgconn.PgError
	if errors.As(err, &pgErr) {
		err = werr.Wrap(err).WithData(
			struct {
				Trigger      *Trigger
				PostgresCode string
				PostgresMsg  string
			}{
				t,
				pgErr.Code,
				pgErr.Message,
			})
		return
	}

	if err != nil {
		err = werr.Wrap(err).WithData(t)
		return
	}

	return
} // }}}
func (t *Trigger) Run() (output any, err error) { // {{{
	datapoints := make(map[string]Datapoint)
	for _, dpname := range t.Datapoints {
		var dp Datapoint
		dp, err = DatapointRetrieve(0, dpname)
		if err != nil {
			err = werr.Wrap(err)
			return
		}
		datapoints[dpname] = dp
	}

	t.DatapointValues = make(map[string]any)
	for dpName, dp := range datapoints {
		t.DatapointValues[dpName] = dp.LastDatapointValue.Value()
	}

	program, err := expr.Compile(t.Expression)
	if err != nil {
		return
	}

	output, err = expr.Run(program, t.DatapointValues)
	if err != nil {
		return
	}
	return
} // }}}
func (t *Trigger) RenameDatapoint(from, to string) error { // {{{
	tree, err := parser.Parse(t.Expression)
	if err != nil {
		return werr.Wrap(err).WithData(t.Expression)
	}

	ast.Walk(&tree.Node, ExprRenamePatcher{from, to})
	t.Expression = tree.Node.String()

	return nil
} // }}}
