package knotfeed

import (
	"cmp"
	"encoding/json"
	"fmt"
	"log/slog"
	"strings"

	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/samber/lo"
	"tangled.org/core/tapc"
)

const (
	legacyRefUpdateNSID          syntax.NSID = "sh.tangled.git.refUpdate"
	legacyCollaboratorUpdateNSID syntax.NSID = "sh.tangled.repo.collaboratorUpdate"

	collaboratorInviteCollection syntax.NSID = "sh.tangled.repo.collaboratorInvite"
)

type legacyAclOp string

const (
	legacyAclOpAdd    legacyAclOp = "add"
	legacyAclOpRemove legacyAclOp = "remove"
)

func (op legacyAclOp) action() tapc.RecordAction {
	return lo.Ternary(strings.EqualFold(string(op), string(legacyAclOpRemove)),
		tapc.RecordDeleteAction, tapc.RecordCreateAction)
}

type legacyFrame struct {
	Nsid    syntax.NSID     `json:"nsid"`
	Created int64           `json:"created"`
	Event   json.RawMessage `json:"event"`
}

type legacyRefUpdate struct {
	Repo         syntax.DID `json:"repo"`
	LegacyRepo   syntax.DID `json:"repoDid"` // knots older than 1.14 name the repo this way
	Ref          string     `json:"ref"`
	OldSha       string     `json:"oldSha"`
	NewSha       string     `json:"newSha"`
	CommitterDid string     `json:"committerDid"`
	PushOptions  []string   `json:"pushOptions"`
}

type legacyAclUpdate struct {
	Op      legacyAclOp `json:"op"`
	Repo    syntax.DID  `json:"repo"`
	Subject syntax.DID  `json:"subject"`
}

var legacyOps = map[syntax.NSID]func(json.RawMessage, *Commit) error{
	legacyRefUpdateNSID:          legacyRefOp,
	legacyCollaboratorUpdateNSID: legacyCollaboratorOp,
}

func decodeLegacy(data []byte, _ *slog.Logger) (Message, error) {
	var frame legacyFrame
	if err := json.Unmarshal(data, &frame); err != nil {
		return Message{}, fmt.Errorf("legacy frame: %w", err)
	}
	if frame.Created <= 0 {
		return Message{}, fmt.Errorf("legacy %s frame lacks its created stamp: %d", frame.Nsid, frame.Created)
	}

	commit := &Commit{Seq: frame.Created}
	if op, known := legacyOps[frame.Nsid]; known {
		if err := op(frame.Event, commit); err != nil {
			return Message{}, err
		}
	}
	return Message{Type: TypeCommit, Commit: commit}, nil
}

func legacyRefOp(payload json.RawMessage, commit *Commit) error {
	var event legacyRefUpdate
	if err := json.Unmarshal(payload, &event); err != nil {
		return fmt.Errorf("legacy ref update: %w", err)
	}
	rkey, short := EscapeRefname(event.Ref)
	newSha, isObject := ParseObjectID(event.NewSha)
	// pre-1.13 knots put the owner's did in repoDid, so this can miss on lookup;
	// the event still has to flow through so the host's cursor keeps moving
	repo := cmp.Or(event.Repo, event.LegacyRepo)
	if event.Ref == "" || repo == "" || !short || !isObject {
		return nil
	}
	op := RecordOp{
		Action:     tapc.RecordDeleteAction,
		Collection: GitRefCollection,
		Rkey:       rkey,
		Prior:      ParsePriorSha(event.OldSha),
	}
	if !newSha.IsZero() {
		// old knots leave committerDid empty; attribute those pushes to the repo itself
		editor, err := syntax.ParseDID(event.CommitterDid)
		if err != nil {
			editor = repo
		}
		record, err := encodeRefRecord(RefRecord{
			Sha:         newSha,
			Editor:      editor,
			PushOptions: event.PushOptions,
		})
		if err != nil {
			return fmt.Errorf("re-encoding legacy ref update: %w", err)
		}
		op.Action, op.Bytes = tapc.RecordCreateAction, record
	}

	commit.Repo = repo
	commit.Records = []RecordOp{op}
	return nil
}

func legacyCollaboratorOp(payload json.RawMessage, commit *Commit) error {
	var event legacyAclUpdate
	if err := json.Unmarshal(payload, &event); err != nil {
		return fmt.Errorf("legacy collaborator update: %w", err)
	}
	if event.Repo == "" || event.Subject == "" {
		return nil
	}

	rkey, err := syntax.ParseRecordKey(event.Subject.String())
	if err != nil {
		return fmt.Errorf("legacy collaborator update names subject %q: %w", event.Subject, err)
	}
	commit.Repo = event.Repo
	commit.Records = []RecordOp{{
		Action:     event.Op.action(),
		Collection: collaboratorInviteCollection,
		Rkey:       rkey,
	}}
	return nil
}
