package db

import (
	"context"
	"errors"
	"fmt"
	"reflect"

	"github.com/pingcap/agent-git-service/internal/tenant"

	"gorm.io/gorm"
	"gorm.io/gorm/schema"
)

var ErrTenantProvenanceMismatch = errors.New("persisted tenant provenance mismatch")

const tenantProvenanceCreateCallback = "ags:set_tenant_provenance"

func tenantProvenanceModels() []any {
	return []any{
		&AuditLogEntry{},
		&DeviceCodeAuditLog{},
		&PagesBuild{},
		&WorkflowRun{},
		&WorkflowRunJob{},
		&HookDelivery{},
		&WikiCompactionJob{},
		&WikiSearchProjectionTask{},
	}
}

// EnforceTenantProvenance backfills legacy records, rejects records that claim
// another tenant, and installs a create guard for all provenance-bearing
// models. It must run after business migrations and before workers or
// listeners can create records.
func EnforceTenantProvenance(database *gorm.DB, requested tenant.Info) error {
	if database == nil {
		return fmt.Errorf("enforce tenant provenance: nil database")
	}
	if err := requested.Validate(); err != nil {
		return fmt.Errorf("enforce tenant provenance: %w", tenant.ErrInvalid)
	}

	owner := string(requested.ID)
	if err := database.Transaction(func(tx *gorm.DB) error {
		for _, model := range tenantProvenanceModels() {
			statement := &gorm.Statement{DB: tx}
			if err := statement.Parse(model); err != nil {
				return fmt.Errorf("parse tenant provenance model: %w", err)
			}
			table := statement.Schema.Table
			if !tx.Migrator().HasTable(model) {
				continue
			}
			if !tx.Migrator().HasColumn(model, "TenantID") {
				return fmt.Errorf("tenant provenance column missing from %s", table)
			}
			if err := tx.Table(table).
				Where("tenant_id IS NULL OR tenant_id = ?", "").
				UpdateColumn("tenant_id", owner).Error; err != nil {
				return fmt.Errorf("backfill tenant provenance for %s: %w", table, err)
			}
			var mismatches int64
			if err := tx.Table(table).
				Where("tenant_id <> ?", owner).
				Count(&mismatches).Error; err != nil {
				return fmt.Errorf("verify tenant provenance for %s: %w", table, err)
			}
			if mismatches != 0 {
				return fmt.Errorf("%w: table %s contains %d record(s) outside tenant %q", ErrTenantProvenanceMismatch, table, mismatches, requested.ID)
			}
		}
		return nil
	}); err != nil {
		return err
	}

	callback := func(tx *gorm.DB) {
		stampTenantProvenance(tx, requested, owner)
	}
	processor := database.Callback().Create()
	if processor.Get(tenantProvenanceCreateCallback) == nil {
		if err := processor.Before("gorm:create").Register(tenantProvenanceCreateCallback, callback); err != nil {
			return fmt.Errorf("register tenant provenance guard: %w", err)
		}
		return nil
	}
	if err := processor.Replace(tenantProvenanceCreateCallback, callback); err != nil {
		return fmt.Errorf("replace tenant provenance guard: %w", err)
	}
	return nil
}

func stampTenantProvenance(tx *gorm.DB, ownerInfo tenant.Info, owner string) {
	if tx == nil || tx.Error != nil || tx.Statement == nil || tx.Statement.Schema == nil {
		return
	}
	field := tx.Statement.Schema.LookUpField("TenantID")
	if field == nil || field.DBName != "tenant_id" {
		return
	}
	ctx := tx.Statement.Context
	if ctx == nil {
		tx.AddError(fmt.Errorf("%w: database operation is missing tenant context", ErrTenantProvenanceMismatch))
		return
	}
	info, err := tenant.Require(ctx)
	if err != nil {
		tx.AddError(fmt.Errorf("%w: %w", ErrTenantProvenanceMismatch, err))
		return
	}
	if !info.Equal(ownerInfo) {
		tx.AddError(fmt.Errorf(
			"%w: context tenant %q does not match database tenant %q",
			ErrTenantProvenanceMismatch,
			info.ID,
			ownerInfo.ID,
		))
		return
	}
	if err := stampTenantProvenanceValue(ctx, field, tx.Statement.ReflectValue, owner); err != nil {
		tx.AddError(err)
	}
}

func stampTenantProvenanceValue(ctx context.Context, field *schema.Field, value reflect.Value, owner string) error {
	for value.IsValid() && (value.Kind() == reflect.Pointer || value.Kind() == reflect.Interface) {
		if value.IsNil() {
			return fmt.Errorf("%w: nil create value", ErrTenantProvenanceMismatch)
		}
		value = value.Elem()
	}
	if !value.IsValid() {
		return fmt.Errorf("%w: invalid create value", ErrTenantProvenanceMismatch)
	}

	switch value.Kind() {
	case reflect.Slice, reflect.Array:
		for index := 0; index < value.Len(); index++ {
			if err := stampTenantProvenanceValue(ctx, field, value.Index(index), owner); err != nil {
				return err
			}
		}
		return nil
	case reflect.Struct:
		current, _ := field.ValueOf(ctx, value)
		currentTenant, ok := current.(string)
		if !ok {
			return fmt.Errorf("%w: tenant_id is not a string", ErrTenantProvenanceMismatch)
		}
		if currentTenant != "" && currentTenant != owner {
			return fmt.Errorf("%w: record=%q database=%q", ErrTenantProvenanceMismatch, currentTenant, owner)
		}
		if err := field.Set(ctx, value, owner); err != nil {
			return fmt.Errorf("set tenant provenance: %w", err)
		}
		return nil
	default:
		return fmt.Errorf("%w: unsupported create value %s", ErrTenantProvenanceMismatch, value.Kind())
	}
}
