package appview

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"slices"
	"strings"

	"github.com/bluesky-social/indigo/atproto/syntax"
	jmodels "github.com/bluesky-social/jetstream/pkg/models"
	"tangled.org/core/api/tangled"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/models"
	"tangled.org/core/orm"
	"tangled.org/core/repoident"
)

func (i *Ingester) ingestRepo(ctx context.Context, e *jmodels.Event, l *slog.Logger) error {
	l = l.With("handler", "ingestRepo")

	switch e.Commit.Operation {
	case jmodels.CommitOperationCreate:
		return i.ingestRepoCreate(ctx, e, l)
	case jmodels.CommitOperationUpdate:
		return i.ingestRepoUpdate(ctx, e, l)
	case jmodels.CommitOperationDelete:
		return i.ingestRepoDelete(ctx, e, l)
	default:
		l.Info("unknown repo operation")
		return nil
	}
}

func (i *Ingester) ingestRepoCreate(ctx context.Context, e *jmodels.Event, l *slog.Logger) error {
	l = l.With("handler", "ingestRepoCreate")

	record := tangled.Repo{}
	if err := json.Unmarshal(json.RawMessage(e.Commit.Record), &record); err != nil {
		l.Error("invalid record", "err", err)
		return err
	}

	if record.RepoDid == nil || *record.RepoDid == "" {
		l.Info("skipping repo create from non-DID-migrated knot")
		return nil
	}
	repoDid, err := syntax.ParseDID(*record.RepoDid)
	if err != nil {
		l.Warn("skipping repo record with malformed repoDid", "value", *record.RepoDid, "err", err)
		return nil
	}

	proceed, err := i.verifyOwnership(ctx, l, repoDid.String(), e.Did, record.Knot)
	if err != nil {
		return err
	}
	if !proceed {
		return nil
	}

	existing, err := db.GetRepo(i.Db,
		orm.FilterEq("did", e.Did),
		orm.FilterEq("rkey", e.Commit.RKey),
	)
	if err == nil {
		l.Info("repo row already exists, skipping create", "did", e.Did, "rkey", e.Commit.RKey)
		if err := i.ensureRepoOwnerPermissions(e.Did, existing.Knot, existing.RepoIdentifier()); err != nil {
			return fmt.Errorf("failed to ensure repo owner permissions: %w", err)
		}
		return nil
	}
	if !errors.Is(err, sql.ErrNoRows) {
		return fmt.Errorf("failed to check existing repo: %w", err)
	}

	prev, err := db.GetRepoByDid(i.Db, repoDid.String())
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return fmt.Errorf("failed to check existing repoDid: %w", err)
	}

	if prev != nil {
		l.Info("repoDid exists under different rkey, renaming",
			"oldRkey", prev.Rkey, "newRkey", e.Commit.RKey)

		oldRepo := *prev

		tx, txErr := i.Db.Begin()
		if txErr != nil {
			return fmt.Errorf("failed to begin rename tx: %w", txErr)
		}
		defer tx.Rollback()

		newName := derefString(record.Name)
		if newName == "" {
			newName = e.Commit.RKey
		}

		if err := db.RenameRepo(tx, e.Did, prev.Rkey, e.Commit.RKey, newName); err != nil {
			return fmt.Errorf("failed to rename repo: %w", err)
		}
		if err := db.RecordRepoRename(tx, e.Did, prev.Rkey, repoDid.String()); err != nil {
			return fmt.Errorf("failed to record rename history: %w", err)
		}
		if err := db.DeleteRepoRename(tx, e.Did, strings.ToLower(newName)); err != nil {
			return fmt.Errorf("failed to clear colliding rename alias: %w", err)
		}

		renamed := *prev
		renamed.Rkey = e.Commit.RKey
		renamed.Name = newName
		desired := repoFromRecord(&renamed, &record)
		if repoMetadataChanged(&renamed, &desired) {
			if err := applyRepoMetadata(tx, &renamed, desired); err != nil {
				return fmt.Errorf("failed to apply metadata after rename: %w", err)
			}
		}

		if err := tx.Commit(); err != nil {
			return fmt.Errorf("failed to commit rename tx: %w", err)
		}

		newRepo, err := db.GetRepo(i.Db,
			orm.FilterEq("did", e.Did),
			orm.FilterEq("rkey", e.Commit.RKey),
		)
		if err != nil {
			l.Warn("failed to fetch repo after rename for notification", "err", err)
			return nil
		}
		if err := i.ensureRepoOwnerPermissions(e.Did, newRepo.Knot, newRepo.RepoIdentifier()); err != nil {
			return fmt.Errorf("failed to ensure repo owner permissions: %w", err)
		}
		i.Notifier.RenameRepo(ctx, syntax.DID(e.Did), &oldRepo, newRepo)
		return nil
	}

	rkey := e.Commit.RKey
	name := derefString(record.Name)
	if name == "" {
		name = rkey
	}

	repo := &models.Repo{
		Did:         e.Did,
		Name:        name,
		Knot:        record.Knot,
		Rkey:        rkey,
		Description: derefString(record.Description),
		Website:     derefString(record.Website),
		Topics:      append([]string(nil), record.Topics...),
		Source:      derefString(record.Source),
		Spindle:     derefString(record.Spindle),
		Labels:      append([]string(nil), record.Labels...),
		RepoDid:     repoDid.String(),
	}

	tx, err := i.Db.Begin()
	if err != nil {
		return fmt.Errorf("failed to begin insert tx: %w", err)
	}
	defer tx.Rollback()

	if err := db.AddRepo(tx, repo); err != nil {
		return fmt.Errorf("failed to insert repo: %w", err)
	}
	if err := db.DeleteRepoRename(tx, e.Did, strings.ToLower(repo.Slug())); err != nil {
		return fmt.Errorf("failed to clear colliding rename alias: %w", err)
	}
	if err := tx.Commit(); err != nil {
		return fmt.Errorf("failed to commit insert tx: %w", err)
	}

	if err := i.ensureRepoOwnerPermissions(e.Did, repo.Knot, repo.RepoIdentifier()); err != nil {
		return fmt.Errorf("failed to ensure repo owner permissions: %w", err)
	}

	i.Notifier.NewRepo(ctx, repo)
	return nil
}

func (i *Ingester) ensureRepoOwnerPermissions(ownerDid, knot, repo string) error {
	if i.Enforcer == nil {
		return fmt.Errorf("ingester has no RBAC enforcer configured")
	}
	if err := i.Enforcer.AddRepo(ownerDid, knot, repo); err != nil {
		return err
	}
	return i.Enforcer.E.SavePolicy()
}

func (i *Ingester) ingestRepoUpdate(ctx context.Context, e *jmodels.Event, l *slog.Logger) error {
	l = l.With("handler", "ingestRepoUpdate")

	record := tangled.Repo{}
	if err := json.Unmarshal(json.RawMessage(e.Commit.Record), &record); err != nil {
		l.Error("invalid record", "err", err)
		return err
	}

	if record.RepoDid == nil || *record.RepoDid == "" {
		l.Info("skipping repo update from non-DID-migrated knot")
		return nil
	}
	_, err := syntax.ParseDID(*record.RepoDid)
	if err != nil {
		l.Warn("skipping repo record with malformed repoDid", "value", *record.RepoDid, "err", err)
		return nil
	}

	proceed, err := i.verifyOwnership(ctx, l, *record.RepoDid, e.Did, record.Knot)
	if err != nil {
		return err
	}
	if !proceed {
		return nil
	}

	current, err := db.GetRepo(i.Db,
		orm.FilterEq("did", e.Did),
		orm.FilterEq("rkey", e.Commit.RKey),
	)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			l.Info("skipping repo update for unknown row")
			return nil
		}
		return fmt.Errorf("failed to fetch repo for ingest: %w", err)
	}

	if current.RepoDid != "" && current.RepoDid != *record.RepoDid {
		l.Warn("rejecting repo update: repoDid is immutable",
			"currentRepoDid", current.RepoDid,
			"recordRepoDid", *record.RepoDid,
		)
		return nil
	}

	desired := repoFromRecord(current, &record)

	if current.Source != desired.Source {
		l.Warn("source field changed but mutation is unsupported, ignoring",
			"current", current.Source, "desired", desired.Source)
	}

	if !repoMetadataChanged(current, &desired) {
		return nil
	}

	tx, err := i.Db.Begin()
	if err != nil {
		return fmt.Errorf("failed to begin tx: %w", err)
	}
	defer tx.Rollback()

	if err := applyRepoMetadata(tx, current, desired); err != nil {
		return fmt.Errorf("failed to apply repo metadata: %w", err)
	}
	if err := tx.Commit(); err != nil {
		return err
	}

	return nil
}

func (i *Ingester) ingestRepoDelete(ctx context.Context, e *jmodels.Event, l *slog.Logger) error {
	l = l.With("handler", "ingestRepoDelete")

	repo, err := db.GetRepo(i.Db,
		orm.FilterEq("did", e.Did),
		orm.FilterEq("rkey", e.Commit.RKey),
	)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			l.Info("skipping repo delete for unknown row")
			return nil
		}
		return fmt.Errorf("failed to fetch repo for delete: %w", err)
	}

	if i.Enforcer == nil {
		return fmt.Errorf("ingester has no RBAC enforcer configured")
	}

	tx, err := i.Db.Begin()
	if err != nil {
		return fmt.Errorf("failed to start txn: %w", err)
	}
	committed := false
	defer func() {
		if committed {
			return
		}
		tx.Rollback()
		i.Enforcer.E.LoadPolicy()
	}()

	if err := db.RemoveRepo(tx, e.Did, e.Commit.RKey); err != nil {
		return fmt.Errorf("failed to delete repo: %w", err)
	}

	if err := i.Enforcer.WipeRepoPolicies(repo.Knot, repo.RepoIdentifier()); err != nil {
		return fmt.Errorf("failed to wipe repo permissions: %w", err)
	}

	if err := tx.Commit(); err != nil {
		return fmt.Errorf("failed to commit txn: %w", err)
	}

	if err := i.Enforcer.E.SavePolicy(); err != nil {
		return fmt.Errorf("failed to save ACLs: %w", err)
	}
	committed = true

	i.Notifier.DeleteRepo(ctx, repo)
	l.Info("deleted repo row")
	return nil
}

func applyRepoMetadata(tx *sql.Tx, current *models.Repo, desired models.Repo) error {
	if err := db.PutRepo(tx, desired); err != nil {
		return err
	}

	if current.Spindle != desired.Spindle {
		var spindlePtr *string
		if desired.Spindle != "" {
			spindlePtr = &desired.Spindle
		}
		if err := db.UpdateSpindle(tx, desired.RepoDid, spindlePtr); err != nil {
			return err
		}
	}

	if !labelsEqual(current.Labels, desired.Labels) {
		if err := reconcileLabels(tx, current, desired); err != nil {
			return err
		}
	}

	return nil
}

func reconcileLabels(tx *sql.Tx, current *models.Repo, desired models.Repo) error {
	added := filterOut(desired.Labels, current.Labels)
	removed := filterOut(current.Labels, desired.Labels)

	if err := applyEach(added, func(l string) error {
		return db.SubscribeLabel(tx, &models.RepoLabel{
			RepoDid: syntax.DID(desired.RepoDid),
			LabelAt: syntax.ATURI(l),
		})
	}); err != nil {
		return err
	}

	return applyEach(removed, func(l string) error {
		return db.UnsubscribeLabel(tx,
			orm.FilterEq("repo_did", desired.RepoDid),
			orm.FilterEq("label_at", l),
		)
	})
}

func filterOut(items, exclude []string) []string {
	return slices.DeleteFunc(slices.Clone(items), func(s string) bool {
		return slices.Contains(exclude, s)
	})
}

func applyEach(items []string, fn func(string) error) error {
	for _, item := range items {
		if err := fn(item); err != nil {
			return err
		}
	}
	return nil
}

func labelsEqual(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	aSorted := append([]string(nil), a...)
	bSorted := append([]string(nil), b...)
	slices.Sort(aSorted)
	slices.Sort(bSorted)
	return slices.Equal(aSorted, bSorted)
}

func repoFromRecord(current *models.Repo, record *tangled.Repo) models.Repo {
	out := *current
	out.Name = derefString(record.Name)
	if out.Name == "" {
		out.Name = current.Rkey
	}
	out.Knot = record.Knot
	out.Description = derefString(record.Description)
	out.Website = derefString(record.Website)
	out.Topics = append([]string(nil), record.Topics...)
	out.Spindle = derefString(record.Spindle)
	out.Source = derefString(record.Source)
	out.Labels = append([]string(nil), record.Labels...)
	if record.RepoDid != nil {
		out.RepoDid = *record.RepoDid
	}
	return out
}

func repoMetadataChanged(current *models.Repo, desired *models.Repo) bool {
	return current.Name != desired.Name ||
		current.Knot != desired.Knot ||
		current.Description != desired.Description ||
		current.Website != desired.Website ||
		current.TopicStr() != desired.TopicStr() ||
		current.Spindle != desired.Spindle ||
		!labelsEqual(current.Labels, desired.Labels)
}

func derefString(s *string) string {
	if s == nil {
		return ""
	}
	return *s
}

func (i *Ingester) verifyOwnership(ctx context.Context, l *slog.Logger, repoDid, eventDid, recordKnot string) (bool, error) {
	if i.Verifier == nil {
		return false, fmt.Errorf("ingester has no repo ownership verifier configured")
	}
	rd, err := repoident.NewRepoDid(repoDid)
	if err != nil {
		l.Warn("rejecting repo event: invalid repoDid on record", "repoDid", repoDid, "err", err)
		return false, nil
	}
	result, err := i.Verifier(ctx, rd)
	if err != nil {
		return false, fmt.Errorf("verify repo ownership: %w", err)
	}
	if result.OwnerDid == "" {
		l.Warn("knot lacks RepoDescribeRepo, skipping owner check; upgrade knot to 1.14+",
			"repoDid", repoDid, "knot", result.KnotURL.String())
	} else if result.OwnerDid.String() != eventDid {
		l.Warn("rejecting repo event: owner mismatch",
			"repoDid", repoDid,
			"claimedOwner", eventDid,
			"knotOwner", result.OwnerDid.String(),
			"knot", result.KnotURL.String(),
		)
		return false, nil
	}
	if !strings.EqualFold(recordKnot, result.KnotURL.Host) {
		l.Warn("rejecting repo event: record knot does not match DID-doc endpoint",
			"repoDid", repoDid,
			"recordKnot", recordKnot,
			"canonicalKnot", result.KnotURL.Host,
		)
		return false, nil
	}
	return true, nil
}
