// Copyright 2019 The Xorm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package core

import (
	"context"
	"database/sql"

	"code.forgejo.org/xorm/xorm/contexts"
)

var _ QueryExecuter = &Tx{}

// Tx represents a transaction
type Tx struct {
	*sql.Tx
	db  *DB
	ctx context.Context
}

// BeginTx begin a transaction with option
func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) {
	hookCtx := contexts.NewContextHook(ctx, "BEGIN TRANSACTION", nil)
	ctx, err := db.beforeProcess(hookCtx)
	if err != nil {
		return nil, err
	}
	tx, err := db.DB.BeginTx(ctx, opts)
	hookCtx.End(ctx, nil, err)
	if err := db.afterProcess(hookCtx); err != nil {
		return nil, err
	}
	return &Tx{tx, db, ctx}, nil
}

// Begin begins a transaction
func (db *DB) Begin() (*Tx, error) {
	return db.BeginTx(context.Background(), nil)
}

// Commit submit the transaction
func (tx *Tx) Commit() error {
	hookCtx := contexts.NewContextHook(tx.ctx, "COMMIT", nil)
	ctx, err := tx.db.beforeProcess(hookCtx)
	if err != nil {
		return err
	}
	err = tx.Tx.Commit()
	hookCtx.End(ctx, nil, err)
	return tx.db.afterProcess(hookCtx)
}

// Rollback rollback the transaction
func (tx *Tx) Rollback() error {
	hookCtx := contexts.NewContextHook(tx.ctx, "ROLLBACK", nil)
	ctx, err := tx.db.beforeProcess(hookCtx)
	if err != nil {
		return err
	}
	err = tx.Tx.Rollback()
	hookCtx.End(ctx, nil, err)
	return tx.db.afterProcess(hookCtx)
}

// PrepareContext prepare the query
func (tx *Tx) PrepareContext(ctx context.Context, query string) (*Stmt, error) {
	names := make(map[string]int)
	var i int
	query = re.ReplaceAllStringFunc(query, func(src string) string {
		names[src[1:]] = i
		i++
		return "?"
	})
	hookCtx := contexts.NewContextHook(ctx, "PREPARE", nil)
	ctx, err := tx.db.beforeProcess(hookCtx)
	if err != nil {
		return nil, err
	}
	stmt, err := tx.Tx.PrepareContext(ctx, query)
	hookCtx.End(ctx, nil, err)
	if err := tx.db.afterProcess(hookCtx); err != nil {
		return nil, err
	}
	return &Stmt{stmt, tx.db, names, query}, nil
}

// Prepare prepare the query
func (tx *Tx) Prepare(query string) (*Stmt, error) {
	return tx.PrepareContext(context.Background(), query)
}

// StmtContext creates Stmt with context
func (tx *Tx) StmtContext(ctx context.Context, stmt *Stmt) *Stmt {
	stmt.Stmt = tx.Tx.StmtContext(ctx, stmt.Stmt)
	return stmt
}

// Stmt creates Stmt
func (tx *Tx) Stmt(stmt *Stmt) *Stmt {
	return tx.StmtContext(context.Background(), stmt)
}

// ExecMapContext executes query with args in a map
func (tx *Tx) ExecMapContext(ctx context.Context, query string, mp any) (sql.Result, error) {
	query, args, err := MapToSlice(query, mp)
	if err != nil {
		return nil, err
	}
	return tx.ExecContext(ctx, query, args...)
}

// ExecMap executes query with args in a map
func (tx *Tx) ExecMap(query string, mp any) (sql.Result, error) {
	return tx.ExecMapContext(context.Background(), query, mp)
}

// ExecStructContext executes query with args in a struct
func (tx *Tx) ExecStructContext(ctx context.Context, query string, st any) (sql.Result, error) {
	query, args, err := StructToSlice(query, st)
	if err != nil {
		return nil, err
	}
	return tx.ExecContext(ctx, query, args...)
}

// ExecContext executes a query with args
func (tx *Tx) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
	hookCtx := contexts.NewContextHook(ctx, query, args)
	ctx, err := tx.db.beforeProcess(hookCtx)
	if err != nil {
		return nil, err
	}
	res, err := tx.Tx.ExecContext(ctx, query, args...)
	hookCtx.End(ctx, res, err)
	if err := tx.db.afterProcess(hookCtx); err != nil {
		return nil, err
	}
	return res, err
}

// ExecStruct executes query with args in a struct
func (tx *Tx) ExecStruct(query string, st any) (sql.Result, error) {
	return tx.ExecStructContext(context.Background(), query, st)
}

// QueryContext query with args
func (tx *Tx) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) {
	hookCtx := contexts.NewContextHook(ctx, query, args)
	ctx, err := tx.db.beforeProcess(hookCtx)
	if err != nil {
		return nil, err
	}
	rows, err := tx.Tx.QueryContext(ctx, query, args...)
	hookCtx.End(ctx, nil, err)
	if err := tx.db.afterProcess(hookCtx); err != nil {
		if rows != nil {
			rows.Close()
		}
		return nil, err
	}
	return &Rows{rows, tx.db}, nil
}

// Query query with args
func (tx *Tx) Query(query string, args ...any) (*Rows, error) {
	return tx.QueryContext(context.Background(), query, args...)
}

// QueryMapContext query with args in a map
func (tx *Tx) QueryMapContext(ctx context.Context, query string, mp any) (*Rows, error) {
	query, args, err := MapToSlice(query, mp)
	if err != nil {
		return nil, err
	}
	return tx.QueryContext(ctx, query, args...)
}

// QueryMap query with args in a map
func (tx *Tx) QueryMap(query string, mp any) (*Rows, error) {
	return tx.QueryMapContext(context.Background(), query, mp)
}

// QueryStructContext query with args in struct
func (tx *Tx) QueryStructContext(ctx context.Context, query string, st any) (*Rows, error) {
	query, args, err := StructToSlice(query, st)
	if err != nil {
		return nil, err
	}
	return tx.QueryContext(ctx, query, args...)
}

// QueryStruct query with args in struct
func (tx *Tx) QueryStruct(query string, st any) (*Rows, error) {
	return tx.QueryStructContext(context.Background(), query, st)
}

// QueryRowContext query one row with args
func (tx *Tx) QueryRowContext(ctx context.Context, query string, args ...any) *Row {
	rows, err := tx.QueryContext(ctx, query, args...)
	return &Row{rows, err}
}

// QueryRow query one row with args
func (tx *Tx) QueryRow(query string, args ...any) *Row {
	return tx.QueryRowContext(context.Background(), query, args...)
}

// QueryRowMapContext query one row with args in a map
func (tx *Tx) QueryRowMapContext(ctx context.Context, query string, mp any) *Row {
	query, args, err := MapToSlice(query, mp)
	if err != nil {
		return &Row{nil, err}
	}
	return tx.QueryRowContext(ctx, query, args...)
}

// QueryRowMap query one row with args in a map
func (tx *Tx) QueryRowMap(query string, mp any) *Row {
	return tx.QueryRowMapContext(context.Background(), query, mp)
}

// QueryRowStructContext query one row with args in struct
func (tx *Tx) QueryRowStructContext(ctx context.Context, query string, st any) *Row {
	query, args, err := StructToSlice(query, st)
	if err != nil {
		return &Row{nil, err}
	}
	return tx.QueryRowContext(ctx, query, args...)
}

// QueryRowStruct query one row with args in struct
func (tx *Tx) QueryRowStruct(query string, st any) *Row {
	return tx.QueryRowStructContext(context.Background(), query, st)
}
