// Package link orchestrates account linking & merge (ARCHITECTURE.md §4).
// It sits above the account, accountmerge and session layers: it verifies the
// caller's control of an identity (an email confirm-code or a gateway-validated
// platform identity), binds a free identity to the current account, and — when the
// identity already has its own account — merges the two. The current account is the
// merge primary, except when the initiator is a guest and the other account is
// durable, in which case the durable account wins and a fresh session is minted for
// it (the client switches to it).
package link

import (
	"context"

	"github.com/google/uuid"

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/accountmerge"
	"scrabble/backend/internal/session"
)

// Service drives the link/merge flow.
type Service struct {
	emails   *account.EmailService
	accounts *account.Store
	merger   *accountmerge.Merger
	sessions *session.Service
}

// NewService constructs a Service over its collaborators.
func NewService(emails *account.EmailService, accounts *account.Store, merger *accountmerge.Merger, sessions *session.Service) *Service {
	return &Service{emails: emails, accounts: accounts, merger: merger, sessions: sessions}
}

// ConfirmResult reports the outcome of a confirm step. Exactly one of Linked,
// MergeRequired or Merged is set. SecondaryID is the account to be retired when a merge
// is required (the caller renders an irreversible-merge confirmation from it). Merged is
// set when the caller was a guest, so the merge ran inline (see confirmOrAutoMerge) and
// Merge carries its result (the switched-session token for the surviving durable account).
type ConfirmResult struct {
	Linked        bool
	MergeRequired bool
	SecondaryID   uuid.UUID
	Merged        bool
	Merge         MergeResult
}

// confirmOrAutoMerge decides a required merge at the confirm step. A durable caller gets
// the explicit MergeRequired confirmation (consolidating two real accounts is irreversible
// and consequential — the user must see it). A GUEST caller does not: by the guest-primary
// rule the durable other account survives and the ephemeral guest is retired, folding its
// games/wallet/stats in — from the user's side it is simply "logged into my account", so the
// merge runs inline and the client just adopts the switched session. The active-game guard can
// still refuse (accountmerge.ErrActiveGameConflict), surfaced to the caller unchanged.
func (s *Service) confirmOrAutoMerge(ctx context.Context, callerID, owner uuid.UUID) (ConfirmResult, error) {
	caller, err := s.accounts.GetByID(ctx, callerID)
	if err != nil {
		return ConfirmResult{}, err
	}
	if !caller.IsGuest {
		return ConfirmResult{MergeRequired: true, SecondaryID: owner}, nil
	}
	res, err := s.merge(ctx, callerID, owner)
	if err != nil {
		return ConfirmResult{}, err
	}
	return ConfirmResult{Merged: true, Merge: res}, nil
}

// MergeResult reports a completed merge. PrimaryID is the surviving account.
// SwitchedToken is a fresh session token for the primary when the active account
// changed (a guest initiator whose durable counterpart won); empty otherwise, in
// which case the caller keeps its current session.
type MergeResult struct {
	PrimaryID     uuid.UUID
	SwitchedToken string
}

// RequestEmail mails a confirm-code for email to the caller (always sent).
func (s *Service) RequestEmail(ctx context.Context, accountID uuid.UUID, email string) error {
	return s.emails.RequestLinkCode(ctx, accountID, email)
}

// ConfirmEmail verifies the code and either binds the free address to the caller
// (Linked) or reports that the address belongs to another account (MergeRequired).
func (s *Service) ConfirmEmail(ctx context.Context, accountID uuid.UUID, email, code string) (ConfirmResult, error) {
	owner, linked, err := s.emails.ConfirmLink(ctx, accountID, email, code)
	if err != nil {
		return ConfirmResult{}, err
	}
	if linked {
		if err := s.accounts.ClearGuest(ctx, accountID); err != nil {
			return ConfirmResult{}, err
		}
		return ConfirmResult{Linked: true}, nil
	}
	return s.confirmOrAutoMerge(ctx, accountID, owner)
}

// MergeEmail re-verifies the code and merges the address's account into the
// caller's (subject to the guest-primary rule).
func (s *Service) MergeEmail(ctx context.Context, callerID uuid.UUID, email, code string) (MergeResult, error) {
	owner, linked, err := s.emails.ConfirmLink(ctx, callerID, email, code)
	if err != nil {
		return MergeResult{}, err
	}
	if linked {
		// Raced to free/self between confirm and merge: it is now simply linked.
		if err := s.accounts.ClearGuest(ctx, callerID); err != nil {
			return MergeResult{}, err
		}
		return MergeResult{PrimaryID: callerID}, nil
	}
	return s.merge(ctx, callerID, owner)
}

// ConfirmTelegram attaches a gateway-validated Telegram identity to the caller
// (Linked) or reports that it belongs to another account (MergeRequired).
func (s *Service) ConfirmTelegram(ctx context.Context, callerID uuid.UUID, externalID string) (ConfirmResult, error) {
	owner, ok, err := s.accounts.AccountIDByIdentity(ctx, account.KindTelegram, externalID)
	if err != nil {
		return ConfirmResult{}, err
	}
	if !ok {
		if err := s.attachTelegram(ctx, callerID, externalID); err != nil {
			return ConfirmResult{}, err
		}
		return ConfirmResult{Linked: true}, nil
	}
	if owner == callerID {
		return ConfirmResult{Linked: true}, nil
	}
	return s.confirmOrAutoMerge(ctx, callerID, owner)
}

// MergeTelegram merges the account owning a gateway-validated Telegram identity
// into the caller's (subject to the guest-primary rule).
func (s *Service) MergeTelegram(ctx context.Context, callerID uuid.UUID, externalID string) (MergeResult, error) {
	owner, ok, err := s.accounts.AccountIDByIdentity(ctx, account.KindTelegram, externalID)
	if err != nil {
		return MergeResult{}, err
	}
	if !ok {
		if err := s.attachTelegram(ctx, callerID, externalID); err != nil {
			return MergeResult{}, err
		}
		return MergeResult{PrimaryID: callerID}, nil
	}
	if owner == callerID {
		return MergeResult{PrimaryID: callerID}, nil
	}
	return s.merge(ctx, callerID, owner)
}

// attachTelegram links the identity to the caller and promotes a guest.
func (s *Service) attachTelegram(ctx context.Context, callerID uuid.UUID, externalID string) error {
	if err := s.accounts.AttachIdentity(ctx, callerID, account.KindTelegram, externalID, true); err != nil {
		return err
	}
	return s.accounts.ClearGuest(ctx, callerID)
}

// ConfirmVK attaches a gateway-validated VK identity to the caller (Linked) or
// reports that it belongs to another account (MergeRequired). The gateway has already
// completed the VK ID code exchange, so externalID is the trusted vk user id.
func (s *Service) ConfirmVK(ctx context.Context, callerID uuid.UUID, externalID string) (ConfirmResult, error) {
	owner, ok, err := s.accounts.AccountIDByIdentity(ctx, account.KindVK, externalID)
	if err != nil {
		return ConfirmResult{}, err
	}
	if !ok {
		if err := s.attachVK(ctx, callerID, externalID); err != nil {
			return ConfirmResult{}, err
		}
		return ConfirmResult{Linked: true}, nil
	}
	if owner == callerID {
		return ConfirmResult{Linked: true}, nil
	}
	return s.confirmOrAutoMerge(ctx, callerID, owner)
}

// MergeVK merges the account owning a gateway-validated VK identity into the caller's
// (subject to the guest-primary rule).
func (s *Service) MergeVK(ctx context.Context, callerID uuid.UUID, externalID string) (MergeResult, error) {
	owner, ok, err := s.accounts.AccountIDByIdentity(ctx, account.KindVK, externalID)
	if err != nil {
		return MergeResult{}, err
	}
	if !ok {
		if err := s.attachVK(ctx, callerID, externalID); err != nil {
			return MergeResult{}, err
		}
		return MergeResult{PrimaryID: callerID}, nil
	}
	if owner == callerID {
		return MergeResult{PrimaryID: callerID}, nil
	}
	return s.merge(ctx, callerID, owner)
}

// attachVK links the identity to the caller and promotes a guest.
func (s *Service) attachVK(ctx context.Context, callerID uuid.UUID, externalID string) error {
	if err := s.accounts.AttachIdentity(ctx, callerID, account.KindVK, externalID, true); err != nil {
		return err
	}
	return s.accounts.ClearGuest(ctx, callerID)
}

// merge decides the primary (the caller, unless it is a guest and the other is
// durable), runs the data merge, retires the secondary's sessions and mints a new
// session when the active account switches.
func (s *Service) merge(ctx context.Context, callerID, otherID uuid.UUID) (MergeResult, error) {
	caller, err := s.accounts.GetByID(ctx, callerID)
	if err != nil {
		return MergeResult{}, err
	}
	primary, secondary := callerID, otherID
	if caller.IsGuest {
		primary, secondary = otherID, callerID
	}
	if err := s.merger.Merge(ctx, primary, secondary); err != nil {
		return MergeResult{}, err
	}
	if err := s.sessions.RevokeAllForAccount(ctx, secondary); err != nil {
		return MergeResult{}, err
	}
	res := MergeResult{PrimaryID: primary}
	if primary != callerID {
		// The switched-to session inherits the caller's current trusted platform
		// (the context the merge was initiated from); absent when the caller's
		// session itself is untrusted.
		platform, _ := session.PlatformFromContext(ctx)
		token, _, err := s.sessions.Create(ctx, primary, platform)
		if err != nil {
			return MergeResult{}, err
		}
		res.SwitchedToken = token
	}
	return res, nil
}
