package crud

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

	"aijianzhan/platform/internal/meta"

	"github.com/google/uuid"
)

// PostgresEngine dynamic row CRUD with tenant_id and optional org_unit_id scope.
type PostgresEngine struct {
	DB   *sql.DB
	Pool *DBPool
}

func NewPostgresEngine(db *sql.DB, pool *DBPool) *PostgresEngine {
	return &PostgresEngine{DB: db, Pool: pool}
}

func (e *PostgresEngine) conn(ref *meta.ResourceRef) (*sql.DB, error) {
	if e.Pool != nil && ref != nil && ref.App != nil {
		return e.Pool.ForApp(ref.App)
	}
	return e.DB, nil
}

func (e *PostgresEngine) List(ctx context.Context, ref *meta.ResourceRef, tenantID int64, page, pageSize int, filters map[string]string, sortBy string) ([]map[string]any, int, error) {
	if err := ensureOp(ref, "list"); err != nil {
		return nil, 0, err
	}
	if err := validateFilters(ref, filters); err != nil {
		return nil, 0, err
	}
	if err := validateSort(ref, sortBy); err != nil {
		return nil, 0, err
	}
	page, pageSize = pageBounds(ref, page, pageSize)
	db, err := e.conn(ref)
	if err != nil {
		return nil, 0, err
	}

	where := []string{`tenant_id = $1`}
	args := []any{tenantID}
	argN := 2
	scope := RowScopeFrom(ctx)
	where, args, argN = appendOrgFilter(where, args, argN, scope.OrgUnitIDs)
	for col, val := range filters {
		where = append(where, fmt.Sprintf("%s = $%d", quoteIdent(col), argN))
		args = append(args, val)
		argN++
	}
	whereSQL := strings.Join(where, " AND ")
	table := qualifiedTable(ref)

	var total int
	countSQL := fmt.Sprintf("SELECT COUNT(1) FROM %s WHERE %s", table, whereSQL)
	if err := db.QueryRowContext(ctx, countSQL, args...).Scan(&total); err != nil {
		return nil, 0, fmt.Errorf("count: %w", err)
	}

	orderSQL := ""
	if sortBy != "" {
		desc := strings.HasPrefix(sortBy, "-")
		field := strings.TrimPrefix(sortBy, "-")
		dir := "ASC"
		if desc {
			dir = "DESC"
		}
		orderSQL = " ORDER BY " + quoteIdent(field) + " " + dir
	}

	offset := (page - 1) * pageSize
	listArgs := append(append([]any{}, args...), pageSize, offset)
	listSQL := fmt.Sprintf(
		"SELECT * FROM %s WHERE %s%s LIMIT $%d OFFSET $%d",
		table, whereSQL, orderSQL, argN, argN+1,
	)
	rows, err := db.QueryContext(ctx, listSQL, listArgs...)
	if err != nil {
		return nil, 0, fmt.Errorf("list: %w", err)
	}
	defer rows.Close()

	items, err := scanRows(rows)
	if err != nil {
		return nil, 0, err
	}
	return items, total, nil
}

func (e *PostgresEngine) Get(ctx context.Context, ref *meta.ResourceRef, tenantID int64, id string) (map[string]any, error) {
	if err := ensureOp(ref, "get"); err != nil {
		return nil, err
	}
	db, err := e.conn(ref)
	if err != nil {
		return nil, err
	}
	pk := ref.Entity.PrimaryKey
	where := []string{`tenant_id = $1`, fmt.Sprintf("%s = $2", quoteIdent(pk))}
	args := []any{tenantID, id}
	argN := 3
	scope := RowScopeFrom(ctx)
	where, args, argN = appendOrgFilter(where, args, argN, scope.OrgUnitIDs)
	_ = argN
	q := fmt.Sprintf(
		"SELECT * FROM %s WHERE %s LIMIT 1",
		qualifiedTable(ref), strings.Join(where, " AND "),
	)
	rows, err := db.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items, err := scanRows(rows)
	if err != nil {
		return nil, err
	}
	if len(items) == 0 {
		return nil, fmt.Errorf("not found")
	}
	return items[0], nil
}

func (e *PostgresEngine) Create(ctx context.Context, ref *meta.ResourceRef, tenantID, userID int64, body map[string]any) (map[string]any, error) {
	if err := ensureOp(ref, "create"); err != nil {
		return nil, err
	}
	db, err := e.conn(ref)
	if err != nil {
		return nil, err
	}
	row, err := sanitizeBody(ref.Entity, body, false)
	if err != nil {
		return nil, err
	}

	pk := ref.Entity.PrimaryKey
	autoPK := isAutoPK(ref.Entity)
	if !autoPK {
		if _, ok := row[pk]; !ok {
			row[pk] = uuid.NewString()
		}
	} else {
		delete(row, pk)
	}

	now := time.Now().UTC()
	row["tenant_id"] = tenantID
	row["created_by"] = userID
	row["created_at"] = now
	row["updated_at"] = now
	if scope := RowScopeFrom(ctx); scope.WriteOrgUnit > 0 {
		row["org_unit_id"] = scope.WriteOrgUnit
	}

	cols := make([]string, 0, len(row))
	placeholders := make([]string, 0, len(row))
	args := make([]any, 0, len(row))
	i := 1
	for k, v := range row {
		cols = append(cols, quoteIdent(k))
		placeholders = append(placeholders, fmt.Sprintf("$%d", i))
		args = append(args, v)
		i++
	}

	q := fmt.Sprintf(
		"INSERT INTO %s (%s) VALUES (%s) RETURNING *",
		qualifiedTable(ref),
		strings.Join(cols, ", "),
		strings.Join(placeholders, ", "),
	)
	rows, err := db.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, fmt.Errorf("insert: %w", err)
	}
	defer rows.Close()
	items, err := scanRows(rows)
	if err != nil {
		return nil, err
	}
	if len(items) == 0 {
		return nil, fmt.Errorf("insert returned no row")
	}
	return items[0], nil
}

func (e *PostgresEngine) Update(ctx context.Context, ref *meta.ResourceRef, tenantID int64, id string, body map[string]any) (map[string]any, error) {
	if err := ensureOp(ref, "update"); err != nil {
		return nil, err
	}
	db, err := e.conn(ref)
	if err != nil {
		return nil, err
	}
	patch, err := sanitizeBody(ref.Entity, body, true)
	if err != nil {
		return nil, err
	}
	delete(patch, ref.Entity.PrimaryKey)
	delete(patch, "tenant_id")
	delete(patch, "org_unit_id")
	if len(patch) == 0 {
		return e.Get(ctx, ref, tenantID, id)
	}
	patch["updated_at"] = time.Now().UTC()

	sets := make([]string, 0, len(patch))
	args := make([]any, 0, len(patch)+2)
	i := 1
	for k, v := range patch {
		sets = append(sets, fmt.Sprintf("%s = $%d", quoteIdent(k), i))
		args = append(args, v)
		i++
	}
	where := []string{fmt.Sprintf("tenant_id = $%d", i), fmt.Sprintf("%s = $%d", quoteIdent(ref.Entity.PrimaryKey), i+1)}
	args = append(args, tenantID, id)
	argN := i + 2
	scope := RowScopeFrom(ctx)
	where, args, argN = appendOrgFilter(where, args, argN, scope.OrgUnitIDs)
	_ = argN
	q := fmt.Sprintf(
		"UPDATE %s SET %s WHERE %s RETURNING *",
		qualifiedTable(ref),
		strings.Join(sets, ", "),
		strings.Join(where, " AND "),
	)
	rows, err := db.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, fmt.Errorf("update: %w", err)
	}
	defer rows.Close()
	items, err := scanRows(rows)
	if err != nil {
		return nil, err
	}
	if len(items) == 0 {
		return nil, fmt.Errorf("not found")
	}
	return items[0], nil
}

func (e *PostgresEngine) Delete(ctx context.Context, ref *meta.ResourceRef, tenantID int64, id string) error {
	if err := ensureOp(ref, "delete"); err != nil {
		return err
	}
	db, err := e.conn(ref)
	if err != nil {
		return err
	}
	where := []string{`tenant_id = $1`, fmt.Sprintf("%s = $2", quoteIdent(ref.Entity.PrimaryKey))}
	args := []any{tenantID, id}
	argN := 3
	scope := RowScopeFrom(ctx)
	where, args, argN = appendOrgFilter(where, args, argN, scope.OrgUnitIDs)
	_ = argN
	q := fmt.Sprintf(
		"DELETE FROM %s WHERE %s",
		qualifiedTable(ref), strings.Join(where, " AND "),
	)
	res, err := db.ExecContext(ctx, q, args...)
	if err != nil {
		return err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return fmt.Errorf("not found")
	}
	return nil
}

func scanRows(rows *sql.Rows) ([]map[string]any, error) {
	cols, err := rows.Columns()
	if err != nil {
		return nil, err
	}
	out := make([]map[string]any, 0)
	for rows.Next() {
		vals := make([]any, len(cols))
		ptrs := make([]any, len(cols))
		for i := range vals {
			ptrs[i] = &vals[i]
		}
		if err := rows.Scan(ptrs...); err != nil {
			return nil, err
		}
		row := make(map[string]any, len(cols))
		for i, c := range cols {
			row[c] = normalizeDBValue(vals[i])
		}
		out = append(out, row)
	}
	return out, rows.Err()
}
