package db

import (
	"database/sql"
	"fmt"

	"tangled.org/core/eventstream"
	"tangled.org/core/notifier"
)

// enough to rebuild the fencing token and workflow identity after a restart
type MillLease struct {
	LeaseID  string
	NodeID   string
	Epoch    string
	Engine   string
	Knot     string
	Rkey     string
	Workflow string
	State    string
}

type ExecutorCursor struct {
	NodeID     string
	Epoch      string
	AckedSeqno uint64
}

type OutboxRow struct {
	Epoch    string
	Seqno    uint64
	Payload  []byte
	ByteSize int64
	Control  bool
}
type OutboxDeletion struct {
	Rows  int64
	Bytes int64
}

func (d *DB) SaveMillLease(l MillLease) error {
	_, err := d.Exec(
		`insert into mill_leases (
			lease_id, node_id, epoch, engine, knot, rkey, workflow, state
		) values (?, ?, ?, ?, ?, ?, ?, ?)
		on conflict(lease_id) do update set state = excluded.state`,
		l.LeaseID, l.NodeID, l.Epoch, l.Engine, l.Knot, l.Rkey, l.Workflow, l.State,
	)
	return err
}

func (d *DB) DeleteMillLease(leaseID string) error {
	_, err := d.Exec(`delete from mill_leases where lease_id = ?`, leaseID)
	return err
}

func (d *DB) ListMillLeases() ([]MillLease, error) {
	rows, err := d.Query(`
		select lease_id, node_id, epoch, engine, knot, rkey, workflow, state
		from mill_leases
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var leases []MillLease
	for rows.Next() {
		var l MillLease
		if err := rows.Scan(
			&l.LeaseID, &l.NodeID, &l.Epoch, &l.Engine, &l.Knot, &l.Rkey, &l.Workflow, &l.State,
		); err != nil {
			return nil, err
		}
		leases = append(leases, l)
	}
	return leases, rows.Err()
}

func (d *DB) ListExecutorCursors() ([]ExecutorCursor, error) {
	rows, err := d.Query(`select node_id, epoch, acked_seqno from mill_executor_cursors`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var cursors []ExecutorCursor
	for rows.Next() {
		var c ExecutorCursor
		if err := rows.Scan(&c.NodeID, &c.Epoch, &c.AckedSeqno); err != nil {
			return nil, err
		}
		cursors = append(cursors, c)
	}
	return cursors, rows.Err()
}

func (d *DB) SetOutboxEpoch(epoch string) error {
	tx, err := d.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	if _, err := tx.Exec(`delete from mill_outbox_rows`); err != nil {
		return err
	}
	if _, err := tx.Exec(`delete from mill_outbox_state`); err != nil {
		return err
	}
	if _, err := tx.Exec(`insert into mill_outbox_state (epoch, next_seqno) values (?, 1)`, epoch); err != nil {
		return err
	}
	return tx.Commit()
}

func (d *DB) AppendOutboxRow(payload []byte, control bool) (uint64, error) {
	tx, err := d.Begin()
	if err != nil {
		return 0, err
	}
	defer tx.Rollback()

	var epoch string
	var nextSeqno uint64
	err = tx.QueryRow(`select epoch, next_seqno from mill_outbox_state limit 1`).Scan(&epoch, &nextSeqno)
	if err == sql.ErrNoRows {
		return 0, fmt.Errorf("no outbox epoch set")
	} else if err != nil {
		return 0, err
	}

	byteSize := int64(len(payload))
	controlVal := 0
	if control {
		controlVal = 1
	}

	if _, err := tx.Exec(
		`insert into mill_outbox_rows (epoch, seqno, payload, byte_size, control) values (?, ?, ?, ?, ?)`,
		epoch, nextSeqno, payload, byteSize, controlVal,
	); err != nil {
		return 0, err
	}

	if _, err := tx.Exec(
		`update mill_outbox_state set next_seqno = ? where epoch = ?`,
		nextSeqno+1, epoch,
	); err != nil {
		return 0, err
	}

	if err := tx.Commit(); err != nil {
		return 0, err
	}
	return nextSeqno, nil
}

func (d *DB) DeleteOutboxPrefix(ackedSeqno uint64) (OutboxDeletion, error) {
	tx, err := d.Begin()
	if err != nil {
		return OutboxDeletion{}, err
	}
	defer tx.Rollback()

	var epoch string
	err = tx.QueryRow(`select epoch from mill_outbox_state limit 1`).Scan(&epoch)
	if err == sql.ErrNoRows {
		return OutboxDeletion{}, nil
	}
	if err != nil {
		return OutboxDeletion{}, err
	}

	var deleted OutboxDeletion
	if err := tx.QueryRow(`
		select count(*),
		       coalesce(sum(byte_size), 0)
		from mill_outbox_rows
		where epoch = ? and seqno <= ?
	`, epoch, ackedSeqno).Scan(&deleted.Rows, &deleted.Bytes); err != nil {
		return OutboxDeletion{}, err
	}
	if _, err := tx.Exec(
		`delete from mill_outbox_rows where epoch = ? and seqno <= ?`,
		epoch, ackedSeqno,
	); err != nil {
		return OutboxDeletion{}, err
	}
	if err := tx.Commit(); err != nil {
		return OutboxDeletion{}, err
	}
	return deleted, nil
}

func (d *DB) ListOutboxRows() ([]OutboxRow, error) {
	rows, err := d.Query(`select epoch, seqno, payload, byte_size, control from mill_outbox_rows order by seqno`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []OutboxRow
	for rows.Next() {
		var r OutboxRow
		var controlVal int
		if err := rows.Scan(&r.Epoch, &r.Seqno, &r.Payload, &r.ByteSize, &controlVal); err != nil {
			return nil, err
		}
		r.Control = (controlVal != 0)
		out = append(out, r)
	}
	return out, rows.Err()
}
func (d *DB) ListOutboxRowsAfter(seqno uint64, limit int) ([]OutboxRow, error) {
	rows, err := d.Query(`
		select epoch, seqno, payload, byte_size, control
		from mill_outbox_rows
		where epoch = (select epoch from mill_outbox_state limit 1)
		  and seqno > ?
		order by seqno
		limit ?
	`, seqno, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []OutboxRow
	for rows.Next() {
		var row OutboxRow
		var control int
		if err := rows.Scan(&row.Epoch, &row.Seqno, &row.Payload, &row.ByteSize, &control); err != nil {
			return nil, err
		}
		row.Control = control != 0
		out = append(out, row)
	}
	return out, rows.Err()
}

func (d *DB) GetOutboxState() (string, uint64, error) {
	var epoch string
	var nextSeqno uint64
	err := d.QueryRow(`select epoch, next_seqno from mill_outbox_state limit 1`).Scan(&epoch, &nextSeqno)
	if err == sql.ErrNoRows {
		return "", 0, nil
	}
	return epoch, nextSeqno, err
}

type EventBatchTx struct {
	tx *sql.Tx
	db *DB
}

func (tx *EventBatchTx) InsertStatusEvent(pipelineAtUri, workflow, status string, workflowError *string, exitCode *int64) error {
	event, err := statusEvent(pipelineAtUri, workflow, status, workflowError, exitCode)
	if err != nil {
		return err
	}
	return eventstream.Insert(tx.tx, event, nil)
}

func (tx *EventBatchTx) DeleteLease(leaseID string) error {
	_, err := tx.tx.Exec(`delete from mill_leases where lease_id = ?`, leaseID)
	return err
}

func (tx *EventBatchTx) AdvanceCursor(nodeID, epoch string, seqno uint64) error {
	_, err := tx.tx.Exec(
		`insert into mill_executor_cursors (node_id, epoch, acked_seqno) values (?, ?, ?)
		 on conflict(node_id, epoch) do update set acked_seqno = excluded.acked_seqno`,
		nodeID, epoch, seqno,
	)
	return err
}

func (tx *EventBatchTx) InsertArtifactRef(leaseID, workflow, ref, hash string) error {
	_, err := tx.tx.Exec(
		`insert into mill_artifacts (lease_id, workflow, ref, hash)
		 values (?, ?, ?, ?)`,
		leaseID, workflow, ref, hash,
	)
	return err
}

type PendingArtifact struct {
	LeaseID  string
	Workflow string
	Status   string
	Error    string
	ExitCode int64
	Ref      string
	Hash     string
}

func (d *DB) SavePendingArtifact(leaseID, workflow, status, errStr string, exitCode int64, ref, hash string) error {
	_, err := d.Exec(
		`insert into executor_pending_artifacts (lease_id, workflow, status, error, exit_code, ref, hash)
		 values (?, ?, ?, ?, ?, ?, ?)
		 on conflict(lease_id) do update set
			workflow = excluded.workflow,
			status = excluded.status,
			error = excluded.error,
			exit_code = excluded.exit_code,
			ref = excluded.ref,
			hash = excluded.hash`,
		leaseID, workflow, status, errStr, exitCode, ref, hash,
	)
	return err
}

func (d *DB) RemovePendingArtifact(leaseID string) error {
	_, err := d.Exec(`delete from executor_pending_artifacts where lease_id = ?`, leaseID)
	return err
}

func (d *DB) ListPendingArtifacts() ([]PendingArtifact, error) {
	rows, err := d.Query(`select lease_id, workflow, status, error, exit_code, ref, hash from executor_pending_artifacts`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var res []PendingArtifact
	for rows.Next() {
		var p PendingArtifact
		if err := rows.Scan(&p.LeaseID, &p.Workflow, &p.Status, &p.Error, &p.ExitCode, &p.Ref, &p.Hash); err != nil {
			return nil, err
		}
		res = append(res, p)
	}
	return res, rows.Err()
}

func (d *DB) ApplyEventBatch(n *notifier.Notifier, fn func(tx *EventBatchTx) error) error {
	tx, err := d.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	batchTx := &EventBatchTx{
		tx: tx,
		db: d,
	}

	if err := fn(batchTx); err != nil {
		return err
	}

	if err := tx.Commit(); err != nil {
		return err
	}

	if n != nil {
		n.NotifyAll()
	}
	return nil
}
