package executor

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"unicode/utf8"

	"github.com/everything-on-mysql/cassandra-on-mysql/pkg/cql/cqlerrors"
	"github.com/everything-on-mysql/cassandra-on-mysql/pkg/cql/parser"
)

const defaultTiDBUserHost = "%"

func (e *ddlExecutor) executeCreateUser(ctx context.Context, user, password string, stmt *parser.CreateUserStatement) (*QueryResult, error) {
	if err := validateTiDBUserName(stmt.Name); err != nil {
		return nil, cqlerrors.Invalid(err.Error())
	}
	if stmt.Superuser != nil && *stmt.Superuser {
		return nil, cqlerrors.Invalid("CREATE USER ... SUPERUSER is not supported in TiDB-native user mode")
	}
	if e.schemaManager != nil {
		if err := e.schemaManager.EnsureCQLMeta(ctx, user, password); err != nil {
			return nil, err
		}
	}

	sql := fmt.Sprintf("CREATE USER %s%s IDENTIFIED BY %s",
		ifNotExistsSQL(stmt.IfNotExists),
		quoteTiDBAccount(stmt.Name, defaultTiDBUserHost),
		quoteTiDBString(stmt.Password))
	conn, release, err := e.getConn(ctx, user, password)
	if err != nil {
		return nil, cqlerrors.Invalid(fmt.Sprintf("failed to get connection: %s", err.Error()))
	}
	if release != nil {
		defer release()
	}
	if _, err := conn.ExecContext(ctx, sql); err != nil {
		return nil, e.mapMySQLErrorWithSQL(ctx, fmt.Errorf("failed to execute DDL: %w", err), mysqlOpWrite, sql)
	}
	return &QueryResult{}, nil
}

func (e *ddlExecutor) executeAlterUser(ctx context.Context, user, password string, stmt *parser.AlterUserStatement) (*QueryResult, error) {
	if err := validateTiDBUserName(stmt.Name); err != nil {
		return nil, cqlerrors.Invalid(err.Error())
	}
	if stmt.Superuser != nil {
		return nil, cqlerrors.Invalid("ALTER USER ... SUPERUSER/NOSUPERUSER is not supported in TiDB-native user mode")
	}
	if stmt.Password == nil {
		return nil, cqlerrors.Invalid("ALTER USER requires WITH PASSWORD in TiDB-native user mode")
	}

	sql := fmt.Sprintf("ALTER USER %s IDENTIFIED BY %s",
		quoteTiDBAccount(stmt.Name, defaultTiDBUserHost),
		quoteTiDBString(*stmt.Password))

	conn, release, err := e.getConn(ctx, user, password)
	if err != nil {
		return nil, cqlerrors.Invalid(fmt.Sprintf("failed to get connection: %s", err.Error()))
	}
	if release != nil {
		defer release()
	}
	if stmt.IfExists {
		exists, err := e.tidbUserExists(ctx, conn, stmt.Name)
		if err != nil {
			return nil, err
		}
		if !exists {
			return &QueryResult{}, nil
		}
	}
	if _, err := conn.ExecContext(ctx, sql); err != nil {
		return nil, e.mapMySQLErrorWithSQL(ctx, fmt.Errorf("failed to execute DDL: %w", err), mysqlOpWrite, sql)
	}
	if e.poolManager != nil {
		e.poolManager.InvalidateUser(stmt.Name)
	}
	return &QueryResult{}, nil
}

func (e *ddlExecutor) executeDropUser(ctx context.Context, user, password string, stmt *parser.DropUserStatement) (*QueryResult, error) {
	if err := validateTiDBUserName(stmt.Name); err != nil {
		return nil, cqlerrors.Invalid(err.Error())
	}
	sql := fmt.Sprintf("DROP USER %s%s",
		ifExistsSQL(stmt.IfExists),
		quoteTiDBAccount(stmt.Name, defaultTiDBUserHost))
	result, err := e.executeDDLWithSQL(ctx, user, password, sql)
	if err != nil {
		return nil, err
	}
	if e.poolManager != nil {
		e.poolManager.InvalidateUser(stmt.Name)
	}
	return result, nil
}

func (e *ddlExecutor) executeListUsers(ctx context.Context, user, password string, stmt *parser.ListUsersStatement) (*QueryResult, error) {
	_ = stmt
	conn, release, err := e.getConn(ctx, user, password)
	if err != nil {
		return nil, cqlerrors.Invalid(fmt.Sprintf("failed to get connection: %s", err.Error()))
	}
	if release != nil {
		defer release()
	}

	rows, err := conn.QueryContext(ctx, "SELECT User FROM mysql.user WHERE User <> '' GROUP BY User ORDER BY User")
	if err != nil {
		return nil, e.mapMySQLErrorWithSQL(ctx, fmt.Errorf("failed to list users: %w", err), mysqlOpRead, "SELECT User FROM mysql.user")
	}
	defer rows.Close()

	result := &QueryResult{
		Columns: []ColumnInfo{
			{Name: "name", Type: "varchar"},
			{Name: "super", Type: "boolean"},
		},
	}
	for rows.Next() {
		var name string
		if err := rows.Scan(&name); err != nil {
			return nil, e.mapMySQLError(ctx, err, mysqlOpRead)
		}
		result.Rows = append(result.Rows, Row{Values: []interface{}{name, false}})
	}
	if err := rows.Err(); err != nil {
		return nil, e.mapMySQLError(ctx, err, mysqlOpRead)
	}
	return result, nil
}

func (e *ddlExecutor) tidbUserExists(ctx context.Context, conn *sql.Conn, name string) (bool, error) {
	var exists int
	err := conn.QueryRowContext(ctx, "SELECT 1 FROM mysql.user WHERE User = ? AND Host = ? LIMIT 1", name, defaultTiDBUserHost).Scan(&exists)
	if err == sql.ErrNoRows {
		return false, nil
	}
	if err != nil {
		return false, e.mapMySQLErrorWithSQL(ctx, fmt.Errorf("failed to check user existence: %w", err), mysqlOpRead, "SELECT 1 FROM mysql.user")
	}
	return true, nil
}

func ifNotExistsSQL(enabled bool) string {
	if enabled {
		return "IF NOT EXISTS "
	}
	return ""
}

func ifExistsSQL(enabled bool) string {
	if enabled {
		return "IF EXISTS "
	}
	return ""
}

func quoteTiDBAccount(user, host string) string {
	return quoteTiDBString(user) + "@" + quoteTiDBString(host)
}

func quoteTiDBString(value string) string {
	return "'" + escapeTiDBString(value) + "'"
}

func escapeTiDBString(value string) string {
	if value == "" {
		return value
	}
	return strings.ReplaceAll(value, "'", "''")
}

func validateTiDBUserName(name string) error {
	if name == "" {
		return fmt.Errorf("user name is required")
	}
	if strings.ContainsRune(name, 0) {
		return fmt.Errorf("user name contains NUL byte")
	}
	if utf8.RuneCountInString(name) > 32 {
		return fmt.Errorf("TiDB user name %q exceeds 32 characters", name)
	}
	return nil
}
