package executor

import (
	"context"
	"fmt"
	"strings"

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

func directBindMarkerExpr(expr parser.Expression) (parser.Expression, bool) {
	switch e := expr.(type) {
	case *parser.BindMarker, *parser.NamedBindMarker:
		return expr, true
	case *parser.TypeHintExpr:
		if isDirectBindMarker(e.Expr) {
			return e.Expr, true
		}
	}
	return nil, false
}

func bindMarkerIndex(expr parser.Expression, markerIndex map[parser.Expression]int) (int, bool, error) {
	markerExpr, ok := directBindMarkerExpr(expr)
	if !ok {
		return 0, false, nil
	}
	idx, ok := markerIndex[markerExpr]
	if !ok {
		return 0, false, fmt.Errorf("bind marker index not found")
	}
	return idx, true, nil
}

func exprHasUnset(expr parser.Expression, markerIndex map[parser.Expression]int, params []interface{}) (bool, error) {
	if expr == nil {
		return false, nil
	}
	switch e := expr.(type) {
	case *parser.BindMarker, *parser.NamedBindMarker:
		idx, ok := markerIndex[expr]
		if !ok {
			return false, fmt.Errorf("bind marker index not found")
		}
		if idx >= len(params) {
			return false, fmt.Errorf("bind marker index out of range")
		}
		return isUnsetParam(params[idx]), nil
	case *parser.TypeHintExpr:
		return exprHasUnset(e.Expr, markerIndex, params)
	case *parser.IsNullExpr:
		return exprHasUnset(e.Expr, markerIndex, params)
	case *parser.AliasedExpr:
		return exprHasUnset(e.Expr, markerIndex, params)
	case *parser.BinaryExpr:
		left, err := exprHasUnset(e.Left, markerIndex, params)
		if err != nil || left {
			return left, err
		}
		return exprHasUnset(e.Right, markerIndex, params)
	case *parser.UnaryExpr:
		return exprHasUnset(e.Expr, markerIndex, params)
	case *parser.FunctionCall:
		for _, arg := range e.Args {
			unset, err := exprHasUnset(arg, markerIndex, params)
			if err != nil || unset {
				return unset, err
			}
		}
		return false, nil
	case *parser.CollectionAccessExpr:
		unset, err := exprHasUnset(e.Target, markerIndex, params)
		if err != nil || unset {
			return unset, err
		}
		unset, err = exprHasUnset(e.Start, markerIndex, params)
		if err != nil || unset {
			return unset, err
		}
		return exprHasUnset(e.End, markerIndex, params)
	case *parser.ListLiteral:
		for _, elem := range e.Elements {
			unset, err := exprHasUnset(elem, markerIndex, params)
			if err != nil || unset {
				return unset, err
			}
		}
		return false, nil
	case *parser.SetLiteral:
		for _, elem := range e.Elements {
			unset, err := exprHasUnset(elem, markerIndex, params)
			if err != nil || unset {
				return unset, err
			}
		}
		return false, nil
	case *parser.TupleLiteral:
		for _, elem := range e.Elements {
			unset, err := exprHasUnset(elem, markerIndex, params)
			if err != nil || unset {
				return unset, err
			}
		}
		return false, nil
	case *parser.VectorLiteral:
		for _, elem := range e.Elements {
			unset, err := exprHasUnset(elem, markerIndex, params)
			if err != nil || unset {
				return unset, err
			}
		}
		return false, nil
	case *parser.MapLiteral:
		for _, entry := range e.Entries {
			unset, err := exprHasUnset(entry.Key, markerIndex, params)
			if err != nil || unset {
				return unset, err
			}
			unset, err = exprHasUnset(entry.Value, markerIndex, params)
			if err != nil || unset {
				return unset, err
			}
		}
		return false, nil
	default:
		return false, nil
	}
}

func remapParamsForStatement(originalIndex map[parser.Expression]int, filtered parser.Statement, params []interface{}) ([]interface{}, error) {
	filteredIndex := buildBindMarkerIndex(filtered)
	if len(filteredIndex) == 0 {
		return nil, nil
	}
	if len(params) == 0 {
		return nil, fmt.Errorf("bind markers require prepared parameters")
	}
	remapped := make([]interface{}, len(filteredIndex))
	for expr, idx := range filteredIndex {
		origIdx, ok := originalIndex[expr]
		if !ok {
			return nil, fmt.Errorf("bind marker index mismatch")
		}
		if origIdx >= len(params) {
			return nil, fmt.Errorf("bind marker index out of range")
		}
		remapped[idx] = params[origIdx]
	}
	return remapped, nil
}

func insertHasUnsetValues(stmt *parser.InsertStatement, markerIndex map[parser.Expression]int, params []interface{}) (bool, error) {
	for _, row := range stmt.Values {
		for _, expr := range row {
			unset, err := exprHasUnset(expr, markerIndex, params)
			if err != nil {
				return false, err
			}
			if unset {
				return true, nil
			}
		}
	}
	return false, nil
}

func filterInsertStatementForUnset(stmt *parser.InsertStatement, params []interface{}, markerIndex map[parser.Expression]int, pkCols []string) (*parser.InsertStatement, []interface{}, bool, bool, error) {
	if stmt == nil {
		return nil, nil, false, false, nil
	}
	if len(stmt.Values) != 1 {
		unset, err := insertHasUnsetValues(stmt, markerIndex, params)
		if err != nil {
			return nil, nil, false, false, err
		}
		if unset {
			return nil, nil, false, false, cqlerrors.Invalid("UNSET is not supported with multiple VALUES")
		}
		return stmt, params, false, false, nil
	}

	pkSet := make(map[string]struct{}, len(pkCols))
	for _, pk := range pkCols {
		pkSet[strings.ToLower(pk)] = struct{}{}
	}

	values := stmt.Values[0]
	if len(values) != len(stmt.Columns) {
		return nil, nil, false, false, fmt.Errorf("VALUES count does not match column count")
	}

	keep := make([]int, 0, len(stmt.Columns))
	hasUnset := false
	for i, col := range stmt.Columns {
		expr := values[i]
		idx, direct, err := bindMarkerIndex(expr, markerIndex)
		if err != nil {
			return nil, nil, false, false, err
		}
		if direct {
			if idx >= len(params) {
				return nil, nil, false, false, fmt.Errorf("bind marker index out of range")
			}
			if isUnsetParam(params[idx]) {
				hasUnset = true
				if _, ok := pkSet[strings.ToLower(col)]; ok {
					return nil, nil, false, false, cqlerrors.Invalid(fmt.Sprintf("UNSET is not allowed for primary key column %q", col))
				}
				continue
			}
		} else {
			unset, err := exprHasUnset(expr, markerIndex, params)
			if err != nil {
				return nil, nil, false, false, err
			}
			if unset {
				return nil, nil, false, false, cqlerrors.Invalid("UNSET is not supported in value expressions")
			}
		}
		keep = append(keep, i)
	}

	if !hasUnset {
		return stmt, params, false, false, nil
	}
	if len(keep) == 0 {
		return nil, nil, true, true, nil
	}

	nonPKRemaining := false
	pkPresent := make(map[string]struct{}, len(pkCols))
	for _, idx := range keep {
		col := stmt.Columns[idx]
		lower := strings.ToLower(col)
		if _, ok := pkSet[lower]; ok {
			pkPresent[lower] = struct{}{}
		} else {
			nonPKRemaining = true
		}
	}
	if !nonPKRemaining {
		for _, pk := range pkCols {
			if _, ok := pkPresent[strings.ToLower(pk)]; !ok {
				return nil, nil, true, true, nil
			}
		}
	}

	filtered := *stmt
	filtered.Columns = make([]string, len(keep))
	filtered.Values = make([][]parser.Expression, 1)
	filtered.Values[0] = make([]parser.Expression, len(keep))
	for i, idx := range keep {
		filtered.Columns[i] = stmt.Columns[idx]
		filtered.Values[0][i] = values[idx]
	}

	remapped, err := remapParamsForStatement(markerIndex, &filtered, params)
	if err != nil {
		return nil, nil, false, false, err
	}
	return &filtered, remapped, true, false, nil
}

func rejectUnsetInConditions(where, ifCond parser.Expression, markerIndex map[parser.Expression]int, params []interface{}) error {
	if where != nil {
		conds, err := extractEqualityConditions(where)
		if err != nil {
			return cqlerrors.Invalid(err.Error())
		}
		for _, cond := range conds {
			if cond.isNull || cond.notNull || cond.value == nil {
				continue
			}
			unset, err := exprHasUnset(cond.value, markerIndex, params)
			if err != nil {
				return err
			}
			if unset {
				return cqlerrors.Invalid("UNSET is not allowed in WHERE clause")
			}
		}
	}
	if ifCond != nil {
		conds, err := extractConditionalConditions(ifCond)
		if err != nil {
			return cqlerrors.Invalid(err.Error())
		}
		for _, cond := range conds {
			if cond.isNull || cond.notNull || cond.value == nil {
				continue
			}
			unset, err := exprHasUnset(cond.value, markerIndex, params)
			if err != nil {
				return err
			}
			if unset {
				return cqlerrors.Invalid("UNSET is not allowed in IF clause")
			}
		}
	}
	return nil
}

func filterUpdateStatementForUnset(stmt *parser.UpdateStatement, params []interface{}, markerIndex map[parser.Expression]int) (*parser.UpdateStatement, []interface{}, bool, bool, error) {
	if stmt == nil {
		return nil, nil, false, false, nil
	}
	if err := rejectUnsetInConditions(stmt.Where, stmt.IfCondition, markerIndex, params); err != nil {
		return nil, nil, false, false, err
	}

	keep := make([]*parser.Assignment, 0, len(stmt.Assigments))
	hasUnset := false
	for _, assignment := range stmt.Assigments {
		expr := assignment.Value
		idx, direct, err := bindMarkerIndex(expr, markerIndex)
		if err != nil {
			return nil, nil, false, false, err
		}
		if direct {
			if idx >= len(params) {
				return nil, nil, false, false, fmt.Errorf("bind marker index out of range")
			}
			if isUnsetParam(params[idx]) {
				hasUnset = true
				continue
			}
		} else {
			unset, err := exprHasUnset(expr, markerIndex, params)
			if err != nil {
				return nil, nil, false, false, err
			}
			if unset {
				return nil, nil, false, false, cqlerrors.Invalid("UNSET is not supported in assignment expressions")
			}
		}
		keep = append(keep, assignment)
	}

	if !hasUnset {
		return stmt, params, false, false, nil
	}
	if len(keep) == 0 {
		return nil, nil, true, true, nil
	}

	filtered := *stmt
	filtered.Assigments = keep
	remapped, err := remapParamsForStatement(markerIndex, &filtered, params)
	if err != nil {
		return nil, nil, false, false, err
	}
	return &filtered, remapped, true, false, nil
}

func (e *dmlExecutor) executeInsertWithUnset(ctx context.Context, exec sqlExecutor, user, password string, stmt *parser.InsertStatement, params []interface{}, plan *dmlPlan) (*QueryResult, error) {
	markerIndex := buildBindMarkerIndex(stmt)
	unsetInValues, err := insertHasUnsetValues(stmt, markerIndex, params)
	if err != nil {
		return nil, err
	}
	if !unsetInValues {
		if plan == nil {
			plan, err = e.buildDMLPlan(ctx, user, password, stmt)
			if err != nil {
				return nil, cqlerrors.Invalid(err.Error())
			}
		}
		return e.executeDMLPlan(ctx, exec, plan, params)
	}

	pkCols, err := e.primaryKeyColumns(ctx, user, password, stmt.Table)
	if err != nil {
		return nil, err
	}

	filtered, remapped, changed, noOp, err := filterInsertStatementForUnset(stmt, params, markerIndex, pkCols)
	if err != nil {
		return nil, err
	}
	if noOp {
		return &QueryResult{}, nil
	}
	if !changed {
		if plan == nil {
			plan, err = e.buildDMLPlan(ctx, user, password, stmt)
			if err != nil {
				return nil, cqlerrors.Invalid(err.Error())
			}
		}
		return e.executeDMLPlan(ctx, exec, plan, params)
	}

	plan, err = e.buildInsertPlan(ctx, user, password, filtered)
	if err != nil {
		return nil, cqlerrors.Invalid(err.Error())
	}
	return e.executeDMLPlan(ctx, exec, plan, remapped)
}

func (e *dmlExecutor) executeUpdateWithUnset(ctx context.Context, exec sqlExecutor, user, password string, stmt *parser.UpdateStatement, params []interface{}, plan *dmlPlan) (*QueryResult, error) {
	markerIndex := buildBindMarkerIndex(stmt)
	filtered, remapped, changed, noOp, err := filterUpdateStatementForUnset(stmt, params, markerIndex)
	if err != nil {
		return nil, err
	}
	if noOp {
		return &QueryResult{}, nil
	}
	if !changed {
		if plan == nil {
			plan, err = e.buildDMLPlan(ctx, user, password, stmt)
			if err != nil {
				return nil, cqlerrors.Invalid(err.Error())
			}
		}
		return e.executeDMLPlan(ctx, exec, plan, params)
	}

	plan, err = e.buildDMLPlan(ctx, user, password, filtered)
	if err != nil {
		return nil, cqlerrors.Invalid(err.Error())
	}
	return e.executeDMLPlan(ctx, exec, plan, remapped)
}

func (e *dmlExecutor) executeDeleteWithUnset(ctx context.Context, exec sqlExecutor, user, password string, stmt *parser.DeleteStatement, params []interface{}, plan *dmlPlan) (*QueryResult, error) {
	markerIndex := buildBindMarkerIndex(stmt)
	if err := rejectUnsetInConditions(stmt.Where, stmt.IfCondition, markerIndex, params); err != nil {
		return nil, err
	}
	if plan == nil {
		var err error
		plan, err = e.buildDMLPlan(ctx, user, password, stmt)
		if err != nil {
			return nil, cqlerrors.Invalid(err.Error())
		}
	}
	return e.executeDMLPlan(ctx, exec, plan, params)
}
