package main

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"fn-registry/functions/infra"
)

// Constants are operator-configurable later via settings UI. Hardcoded for issue 0147 MVP.
const (
	homeserverURL       = "https://matrix-af2f3d.organic-machine.com"
	masIssuer           = "https://auth-af2f3d.organic-machine.com/"
	masClientID         = "VDC4XQ2ZKN2TJ0BYVJ54FK7M6Y"
	loopbackPort        = 8765
	keyringServiceName  = "fn_registry.matrix_client_pc"
	oidcTimeoutSeconds  = 300
)

var defaultScopes = []string{
	"openid",
	"urn:matrix:org.matrix.msc2967.client:api:*",
}

// MatrixService is bound to the Wails frontend.
type MatrixService struct {
	ctx   context.Context
	mu    sync.Mutex
	store *infra.KeyringTokenStore
}

func NewMatrixService() *MatrixService {
	return &MatrixService{
		store: infra.NewKeyringTokenStore(keyringServiceName),
	}
}

func (s *MatrixService) SetContext(ctx context.Context) {
	s.ctx = ctx
}

// SessionView is the safe-to-send JSON for the frontend (no tokens).
type SessionView struct {
	UserID        string `json:"user_id"`
	DeviceID      string `json:"device_id"`
	HomeserverURL string `json:"homeserver_url"`
	HasToken      bool   `json:"has_token"`
	ExpiresAt     string `json:"expires_at,omitempty"`
}

// Login launches the OAuth2 PKCE flow against MAS. Blocks until completion or timeout.
// Returns the user_id of the authenticated session.
func (s *MatrixService) Login() (string, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	cfg := infra.MasOidcLoopbackConfig{
		Issuer:         masIssuer,
		ClientID:       masClientID,
		Scopes:         defaultScopes,
		LoopbackPort:   loopbackPort,
		OpenBrowser:    true,
		TimeoutSeconds: oidcTimeoutSeconds,
	}
	res, err := infra.MasOidcLoopback(cfg)
	if err != nil {
		return "", fmt.Errorf("oidc: %w", err)
	}

	// Initialize Matrix client to discover user_id + device_id via whoami.
	tmpStore := tempStoreDir()
	clientCfg := infra.MatrixClientInitConfig{
		HomeserverURL: homeserverURL,
		// UserID is unknown until whoami. mautrix-go requires it pre-set, but we'll
		// use Whoami via the Wails service directly. As shortcut: parse id_token if present.
		// For v0.1.0 use a placeholder + Whoami after; mautrix accepts empty UserID, then
		// updates after whoami call.
		UserID:       "",
		AccessToken:  res.AccessToken,
		StoreDir:     tmpStore,
		EnableCrypto: false,
	}

	// Pre-fetch user_id by hitting /whoami directly (mautrix requires UserID at NewClient).
	userID, deviceID, err := whoami(s.ctx, homeserverURL, res.AccessToken)
	if err != nil {
		return "", fmt.Errorf("whoami: %w", err)
	}
	clientCfg.UserID = userID
	clientCfg.DeviceID = deviceID
	clientCfg.StoreDir = userStoreDir(userID)

	if _, err := infra.MatrixClientInit(clientCfg); err != nil {
		return "", fmt.Errorf("matrix init: %w", err)
	}

	tok := infra.Token{
		AccessToken:   res.AccessToken,
		RefreshToken:  res.RefreshToken,
		UserID:        userID,
		DeviceID:      deviceID,
		HomeserverURL: homeserverURL,
		Issuer:        masIssuer,
		ClientID:      masClientID,
	}
	if res.ExpiresIn > 0 {
		tok.ExpiresAt = time.Now().Add(time.Duration(res.ExpiresIn) * time.Second)
	}
	if err := s.store.Save(userID, tok); err != nil {
		return "", fmt.Errorf("keyring save: %w", err)
	}
	return userID, nil
}

// GetSession returns the persisted session for the given user_id (or last-known if empty).
func (s *MatrixService) GetSession(userID string) (*SessionView, error) {
	if userID == "" {
		// v0.1.0: no multi-account index. Frontend must pass the user_id once known.
		return nil, errors.New("user_id required (v0.1.0 multi-account index TODO)")
	}
	tok, err := s.store.Load(userID)
	if err != nil {
		if errors.Is(err, infra.ErrNotFound) {
			return nil, nil
		}
		return nil, fmt.Errorf("keyring load: %w", err)
	}
	view := &SessionView{
		UserID:        tok.UserID,
		DeviceID:      tok.DeviceID,
		HomeserverURL: tok.HomeserverURL,
		HasToken:      tok.AccessToken != "",
	}
	if !tok.ExpiresAt.IsZero() {
		view.ExpiresAt = tok.ExpiresAt.Format(time.RFC3339)
	}
	return view, nil
}

// Logout deletes the persisted token for the given user_id.
func (s *MatrixService) Logout(userID string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if userID == "" {
		return errors.New("user_id required")
	}
	return s.store.Delete(userID)
}
