package packs

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"sort"
	"strings"
)

type sourceBackedPageLayoutBusinessKey struct {
	ObjectAPIName string
	LayoutName    string
}

type sourceBackedPageLayoutPackBatch struct {
	declaredNonDefault []sourceBackedPageLayoutBusinessKey
}

func newSourceBackedPageLayoutPackSweeperComponent(packName string, batch *sourceBackedPageLayoutPackBatch) PackComponent {
	manifestComponent := ManifestComponent{
		Kind:          ComponentKindPageLayout,
		BusinessKey:   "_pack_sweep_non_default",
		Manageability: ManageabilityDefaultsOnly,
	}
	return &sourceBackedManagedComponent{
		packName:          packName,
		manifestComponent: manifestComponent,
		apply: func(ctx context.Context, deps ApplyDeps) error {
			return reconcileSourceBackedPageLayoutDrops(ctx, deps.DB, batch, packName)
		},
		verify: func(ctx context.Context, deps VerifyDeps) error {
			return verifySourceBackedPageLayoutPackSweeper(ctx, deps.DB, batch, packName)
		},
	}
}

func verifySourceBackedPageLayoutPackSweeper(ctx context.Context, db *sql.DB, batch *sourceBackedPageLayoutPackBatch, ownerPack string) error {
	leftover, err := queryUndeclaredSourceBackedPageLayoutRows(ctx, db, batch, ownerPack)
	if err != nil {
		return err
	}
	if len(leftover) == 0 {
		return nil
	}
	labels := make([]string, 0, len(leftover))
	for _, row := range leftover {
		if err := failIfLayoutAssignmentsStillReferenceLayout(ctx, db, row); err != nil {
			return err
		}
		labels = append(labels, row.ObjectAPIName+"/"+row.LayoutName)
	}
	sort.Strings(labels)
	return &sourceBackedPageLayoutRepairableDriftError{
		ownerPack: ownerPack,
		labels:    labels,
	}
}

type sourceBackedPageLayoutRepairableDriftError struct {
	ownerPack string
	labels    []string
}

func (e *sourceBackedPageLayoutRepairableDriftError) Error() string {
	return fmt.Sprintf(
		"pack %s has %d undeclared non-default page_layout row(s) left live: %s; run 'go run ./cmd/setup apply'",
		e.ownerPack, len(e.labels), strings.Join(e.labels, ", "),
	)
}

func IsSourceBackedPageLayoutRepairableDrift(err error) bool {
	var target *sourceBackedPageLayoutRepairableDriftError
	return errors.As(err, &target)
}

func reconcileSourceBackedPageLayoutDrops(ctx context.Context, db *sql.DB, batch *sourceBackedPageLayoutPackBatch, ownerPack string) error {
	leftover, err := queryUndeclaredSourceBackedPageLayoutRows(ctx, db, batch, ownerPack)
	if err != nil {
		return err
	}
	if len(leftover) == 0 {
		return nil
	}
	for _, row := range leftover {
		if err := failIfLayoutAssignmentsStillReferenceLayout(ctx, db, row); err != nil {
			return err
		}
	}
	sort.Slice(leftover, func(i, j int) bool { return leftover[i].ID < leftover[j].ID })
	placeholders := strings.TrimRight(strings.Repeat("?,", len(leftover)), ",")
	args := make([]any, 0, len(leftover)+1)
	args = append(args, ownerPack)
	for _, row := range leftover {
		args = append(args, row.ID)
	}
	query := `
UPDATE _System_PageLayout
SET IsDeleted = 1, IsDefault = 0, LastModifiedDate = CURRENT_TIMESTAMP
WHERE OwnerPack = ?
  AND COALESCE(IsDeleted, 0) = 0
  AND (IsCustomized = 0 OR IsCustomized IS NULL)
  AND Id IN (` + placeholders + `)`
	if _, err := db.ExecContext(ctx, query, args...); err != nil {
		return fmt.Errorf("reconcile dropped non-default page_layout rows for pack %s: %w", ownerPack, err)
	}
	return nil
}

type sourceBackedPageLayoutOwnedRow struct {
	ID            string
	ObjectAPIName string
	LayoutName    string
	IsDefault     bool
}

func failIfLayoutAssignmentsStillReferenceLayout(ctx context.Context, db *sql.DB, row sourceBackedPageLayoutOwnedRow) error {
	if row.IsDefault {
		return fmt.Errorf(
			"undeclared non-default page_layout %s/%s row %s is still the live wildcard default route",
			row.ObjectAPIName, row.LayoutName, row.ID,
		)
	}
	refs, err := queryLayoutAssignmentReferences(ctx, db, row.ID)
	if err != nil {
		return err
	}
	if len(refs) == 0 {
		return nil
	}
	sort.Strings(refs)
	return fmt.Errorf(
		"undeclared non-default page_layout %s/%s row %s still has %d layout assignment reference(s): %s",
		row.ObjectAPIName, row.LayoutName, row.ID, len(refs), strings.Join(refs, ", "),
	)
}

func queryUndeclaredSourceBackedPageLayoutRows(ctx context.Context, db *sql.DB, batch *sourceBackedPageLayoutPackBatch, ownerPack string) ([]sourceBackedPageLayoutOwnedRow, error) {
	if db == nil {
		return nil, fmt.Errorf("database is required")
	}
	declared := map[string]struct{}{}
	if batch != nil {
		for _, key := range batch.declaredNonDefault {
			declared[normalizedSourceBackedPageLayoutTupleKey(key.ObjectAPIName, key.LayoutName)] = struct{}{}
		}
	}
	rows, err := db.QueryContext(ctx, `
SELECT Id, ObjectApiName, LayoutName, COALESCE(IsDefault, 0)
FROM _System_PageLayout
WHERE OwnerPack = ?
  AND COALESCE(IsDeleted, 0) = 0
  AND (IsCustomized = 0 OR IsCustomized IS NULL)`, ownerPack)
	if err != nil {
		return nil, fmt.Errorf("query owner-pack scoped non-default page_layout rows: %w", err)
	}
	defer rows.Close()

	var leftover []sourceBackedPageLayoutOwnedRow
	for rows.Next() {
		var row sourceBackedPageLayoutOwnedRow
		var isDefault int
		if err := rows.Scan(&row.ID, &row.ObjectAPIName, &row.LayoutName, &isDefault); err != nil {
			return nil, fmt.Errorf("scan owner-pack scoped non-default page_layout row: %w", err)
		}
		row.IsDefault = isDefault != 0
		if row.ID == sourceBackedLayoutID(row.ObjectAPIName, "", true) {
			continue
		}
		if _, ok := declared[normalizedSourceBackedPageLayoutTupleKey(row.ObjectAPIName, row.LayoutName)]; ok {
			continue
		}
		leftover = append(leftover, row)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate owner-pack scoped non-default page_layout rows: %w", err)
	}
	return leftover, nil
}

func queryLayoutAssignmentReferences(ctx context.Context, db *sql.DB, layoutID string) ([]string, error) {
	rows, err := db.QueryContext(ctx, `
SELECT Id, ObjectApiName, COALESCE(ProfileId, ''), COALESCE(RecordTypeId, ''), COALESCE(RoleId, ''), COALESCE(RouteSource, '')
FROM _System_LayoutAssignment
WHERE LayoutId = ?`, layoutID)
	if err != nil {
		return nil, fmt.Errorf("query layout assignment references for page_layout %s: %w", layoutID, err)
	}
	defer rows.Close()

	var refs []string
	for rows.Next() {
		var id, objectAPIName, profileID, recordTypeID, roleID, routeSource string
		if err := rows.Scan(&id, &objectAPIName, &profileID, &recordTypeID, &roleID, &routeSource); err != nil {
			return nil, fmt.Errorf("scan layout assignment reference for page_layout %s: %w", layoutID, err)
		}
		refs = append(refs, fmt.Sprintf("%s:%s/%s/%s/%s/%s", id, objectAPIName, profileID, recordTypeID, roleID, routeSource))
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate layout assignment references for page_layout %s: %w", layoutID, err)
	}
	return refs, nil
}
