package coredb

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/jmoiron/sqlx"
)

const sqlxDriverName = "mysql"

type sqlxDB struct {
	db *sqlx.DB
}

type sqlxTx struct {
	tx *sqlx.Tx
}

func newSQLXDB(db *sql.DB) *sqlxDB {
	if db == nil {
		return nil
	}
	return &sqlxDB{db: sqlx.NewDb(db, sqlxDriverName)}
}

func (db *sqlxDB) sqlDB() *sql.DB {
	if db == nil || db.db == nil {
		return nil
	}
	return db.db.DB
}

func (db *sqlxDB) Database() string {
	return "tidb"
}

func (db *sqlxDB) Ping(ctx context.Context) error {
	if db == nil || db.db == nil {
		return fmt.Errorf("tidb db is nil")
	}
	return db.db.PingContext(ctx)
}

func (db *sqlxDB) Close() {
	if db != nil && db.db != nil {
		_ = db.db.Close()
	}
}

func (db *sqlxDB) QueryRow(ctx context.Context, query string, args ...any) Row {
	if db == nil || db.db == nil {
		return errorRow{err: fmt.Errorf("tidb db is nil")}
	}

	reboundQuery, reboundArgs, err := rebindPositionalQuery(query, args)
	if err != nil {
		return errorRow{err: err}
	}
	return sqlRow{row: db.db.QueryRowContext(ctx, reboundQuery, reboundArgs...)}
}

func (db *sqlxDB) Query(ctx context.Context, query string, args ...any) (Rows, error) {
	if db == nil || db.db == nil {
		return nil, fmt.Errorf("tidb db is nil")
	}

	reboundQuery, reboundArgs, err := rebindPositionalQuery(query, args)
	if err != nil {
		return nil, err
	}
	rows, err := db.db.QueryContext(ctx, reboundQuery, reboundArgs...)
	if err != nil {
		return nil, err
	}
	return &sqlRowsAdapter{rows: rows}, nil
}

func (db *sqlxDB) Exec(ctx context.Context, query string, args ...any) (Result, error) {
	if db == nil || db.db == nil {
		return nil, fmt.Errorf("tidb db is nil")
	}

	reboundQuery, reboundArgs, err := rebindPositionalQuery(query, args)
	if err != nil {
		return nil, err
	}
	result, err := db.db.ExecContext(ctx, reboundQuery, reboundArgs...)
	if err != nil {
		return nil, err
	}
	return resultFromSQL(result)
}

func (db *sqlxDB) BeginTx(ctx context.Context, options TxOptions) (Tx, error) {
	if db == nil || db.db == nil {
		return nil, fmt.Errorf("tidb db is nil")
	}

	sqlOptions, err := sqlTxOptionsFromContract(options)
	if err != nil {
		return nil, err
	}
	tx, err := db.db.BeginTxx(ctx, sqlOptions)
	if err != nil {
		return nil, err
	}
	return &sqlxTx{tx: tx}, nil
}

func (db *sqlxDB) Get(ctx context.Context, dest any, query string, args ...any) error {
	if db == nil || db.db == nil {
		return fmt.Errorf("tidb db is nil")
	}

	reboundQuery, reboundArgs, err := rebindPositionalQuery(query, args)
	if err != nil {
		return err
	}
	return db.db.GetContext(ctx, dest, reboundQuery, reboundArgs...)
}

func (db *sqlxDB) Select(ctx context.Context, dest any, query string, args ...any) error {
	if db == nil || db.db == nil {
		return fmt.Errorf("tidb db is nil")
	}

	reboundQuery, reboundArgs, err := rebindPositionalQuery(query, args)
	if err != nil {
		return err
	}
	return db.db.SelectContext(ctx, dest, reboundQuery, reboundArgs...)
}

func (db *sqlxDB) NamedExec(ctx context.Context, query string, arg any) (Result, error) {
	if db == nil || db.db == nil {
		return nil, fmt.Errorf("tidb db is nil")
	}
	result, err := db.db.NamedExecContext(ctx, query, arg)
	if err != nil {
		return nil, err
	}
	return resultFromSQL(result)
}

func (tx *sqlxTx) Database() string {
	return "tidb"
}

func (tx *sqlxTx) Exec(ctx context.Context, query string, args ...any) (Result, error) {
	if tx == nil || tx.tx == nil {
		return nil, fmt.Errorf("tidb tx is nil")
	}

	reboundQuery, reboundArgs, err := rebindPositionalQuery(query, args)
	if err != nil {
		return nil, err
	}
	result, err := tx.tx.ExecContext(ctx, reboundQuery, reboundArgs...)
	if err != nil {
		return nil, err
	}
	return resultFromSQL(result)
}

func (tx *sqlxTx) Query(ctx context.Context, query string, args ...any) (Rows, error) {
	if tx == nil || tx.tx == nil {
		return nil, fmt.Errorf("tidb tx is nil")
	}

	reboundQuery, reboundArgs, err := rebindPositionalQuery(query, args)
	if err != nil {
		return nil, err
	}
	rows, err := tx.tx.QueryContext(ctx, reboundQuery, reboundArgs...)
	if err != nil {
		return nil, err
	}
	return &sqlRowsAdapter{rows: rows}, nil
}

func (tx *sqlxTx) QueryRow(ctx context.Context, query string, args ...any) Row {
	if tx == nil || tx.tx == nil {
		return errorRow{err: fmt.Errorf("tidb tx is nil")}
	}

	reboundQuery, reboundArgs, err := rebindPositionalQuery(query, args)
	if err != nil {
		return errorRow{err: err}
	}
	return sqlRow{row: tx.tx.QueryRowContext(ctx, reboundQuery, reboundArgs...)}
}

func (tx *sqlxTx) Commit(context.Context) error {
	if tx == nil || tx.tx == nil {
		return fmt.Errorf("tidb tx is nil")
	}
	return tx.tx.Commit()
}

func (tx *sqlxTx) Rollback(context.Context) error {
	if tx == nil || tx.tx == nil {
		return fmt.Errorf("tidb tx is nil")
	}
	return tx.tx.Rollback()
}

func (tx *sqlxTx) Get(ctx context.Context, dest any, query string, args ...any) error {
	if tx == nil || tx.tx == nil {
		return fmt.Errorf("tidb tx is nil")
	}

	reboundQuery, reboundArgs, err := rebindPositionalQuery(query, args)
	if err != nil {
		return err
	}
	return tx.tx.GetContext(ctx, dest, reboundQuery, reboundArgs...)
}

func (tx *sqlxTx) Select(ctx context.Context, dest any, query string, args ...any) error {
	if tx == nil || tx.tx == nil {
		return fmt.Errorf("tidb tx is nil")
	}

	reboundQuery, reboundArgs, err := rebindPositionalQuery(query, args)
	if err != nil {
		return err
	}
	return tx.tx.SelectContext(ctx, dest, reboundQuery, reboundArgs...)
}

func (tx *sqlxTx) NamedExec(ctx context.Context, query string, arg any) (Result, error) {
	if tx == nil || tx.tx == nil {
		return nil, fmt.Errorf("tidb tx is nil")
	}
	result, err := tx.tx.NamedExecContext(ctx, query, arg)
	if err != nil {
		return nil, err
	}
	return resultFromSQL(result)
}

func resultFromSQL(result sql.Result) (Result, error) {
	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return nil, err
	}
	return commandTagFromRowsAffected(rowsAffected), nil
}
