package xrpc

import (
	"encoding/json"
	"log/slog"
	"net/http"
	"runtime/debug"

	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/go-chi/chi/v5"
	"tangled.org/core/api/tangled"
	"tangled.org/core/appview/cloudflare"
	"tangled.org/core/appview/codesearch"
	"tangled.org/core/appview/config"
	"tangled.org/core/appview/db"
	whnotify "tangled.org/core/appview/notify/webhook"
	"tangled.org/core/idresolver"
	xrpcerr "tangled.org/core/xrpc/errors"
	"tangled.org/core/xrpc/serviceauth"
)

const ActorDid = serviceauth.ActorDid

type Xrpc struct {
	DB          *db.DB
	Config      *config.Config
	Logger      *slog.Logger
	ServiceAuth *serviceauth.ServiceAuth
	IdResolver  *idresolver.Resolver
	Cloudflare  *cloudflare.Client
	CodeSearch  *codesearch.CodeSearch
	Webhooks    *whnotify.Notifier

	// reserved usernames rejected at signup completion
	DisallowedNicknames map[string]bool
}

func (x *Xrpc) Router() http.Handler {
	r := chi.NewRouter()

	r.Use(x.cors)

	// health check, atproto _health convention
	r.Get("/_health", x.health)

	// open endpoints: signup happens pre-identity, so no service auth
	r.Post("/"+tangled.TempAccountBeginSignupNSID, x.AccountBeginSignup)
	r.Post("/"+tangled.TempAccountCompleteSignupNSID, x.AccountCompleteSignup)

	// authenticated endpoints
	r.Group(func(r chi.Router) {
		r.Use(x.ServiceAuth.VerifyServiceAuth)

		// code search is gated on login, matching the appview ui
		r.Get("/"+tangled.TempSearchSearchCodeNSID, x.SearchSearchCode)

		// notifications
		r.Get("/"+tangled.TempNotificationListNotificationsNSID, x.NotificationList)
		r.Get("/"+tangled.TempNotificationGetUnreadCountNSID, x.NotificationGetUnreadCount)
		r.Post("/"+tangled.TempNotificationUpdateSeenNSID, x.NotificationUpdateSeen)
		r.Post("/"+tangled.TempNotificationMarkAllReadNSID, x.NotificationMarkAllRead)
		r.Post("/"+tangled.TempNotificationDeleteNotificationNSID, x.NotificationDelete)
		r.Get("/"+tangled.TempNotificationGetPreferencesNSID, x.NotificationGetPreferences)
		r.Post("/"+tangled.TempNotificationUpdatePreferencesNSID, x.NotificationUpdatePreferences)

		// focus mode
		r.Post("/"+tangled.TempFocusBeginSessionNSID, x.FocusBegin)
		r.Post("/"+tangled.TempFocusNextItemNSID, x.FocusNext)
		r.Post("/"+tangled.TempFocusEndSessionNSID, x.FocusEnd)

		// account management
		r.Get("/"+tangled.TempAccountListEmailsNSID, x.AccountListEmails)
		r.Post("/"+tangled.TempAccountDeleteEmailNSID, x.AccountDeleteEmail)
		r.Post("/"+tangled.TempAccountSetPrimaryEmailNSID, x.AccountSetPrimaryEmail)
		r.Post("/"+tangled.TempAccountSubscribeNewsletterNSID, x.AccountSubscribeNewsletter)
		r.Post("/"+tangled.TempAccountDismissNewsletterNSID, x.AccountDismissNewsletter)

		// webhooks
		r.Get("/"+tangled.TempRepoListWebhooksNSID, x.WebhookList)
		r.Post("/"+tangled.TempRepoCreateWebhookNSID, x.WebhookCreate)
		r.Post("/"+tangled.TempRepoUpdateWebhookNSID, x.WebhookUpdate)
		r.Post("/"+tangled.TempRepoDeleteWebhookNSID, x.WebhookDelete)
		r.Post("/"+tangled.TempRepoToggleWebhookNSID, x.WebhookToggle)
		r.Get("/"+tangled.TempRepoListWebhookDeliveriesNSID, x.WebhookListDeliveries)
		r.Post("/"+tangled.TempRepoRetryWebhookDeliveryNSID, x.WebhookRetryDelivery)

		// sites
		r.Get("/"+tangled.TempSiteGetDomainClaimNSID, x.SiteGetDomainClaim)
		r.Post("/"+tangled.TempSiteClaimDomainNSID, x.SiteClaimDomain)
		r.Post("/"+tangled.TempSiteReleaseDomainNSID, x.SiteReleaseDomain)
		r.Get("/"+tangled.TempRepoGetSiteConfigNSID, x.SiteGetRepoSiteConfig)
		r.Post("/"+tangled.TempRepoUpdateSiteConfigNSID, x.SiteUpdateRepoSiteConfig)
		r.Post("/"+tangled.TempRepoDisableSiteNSID, x.SiteDisableRepoSite)
	})

	return r
}

// timeFormat is the datetime format used across lexicon output fields
const timeFormat = "2006-01-02T15:04:05.000Z"

// health responds to /xrpc/_health with the running version
func (x *Xrpc) health(w http.ResponseWriter, r *http.Request) {
	x.writeJSON(w, map[string]string{"version": serviceVersion()})
}

// serviceVersion returns the build's vcs revision, or "dev"
func serviceVersion() string {
	info, ok := debug.ReadBuildInfo()
	if !ok {
		return "dev"
	}
	for _, s := range info.Settings {
		if s.Key == "vcs.revision" && s.Value != "" {
			return s.Value
		}
	}
	return "dev"
}

// cors allows the browser origin to call the xrpc endpoints. auth is via
// bearer tokens, not cookies, so a wildcard origin is safe.
func (x *Xrpc) cors(next http.Handler) http.Handler {
	origin := x.Config.Core.XrpcCorsOrigin
	if origin == "" {
		origin = "*"
	}
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", origin)
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
		w.Header().Set("Access-Control-Max-Age", "86400")
		if origin != "*" {
			w.Header().Add("Vary", "Origin")
		}
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusNoContent)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(e)
}

func (x *Xrpc) writeJSON(w http.ResponseWriter, v any) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(v)
}

func actorDid(r *http.Request) (string, bool) {
	did, ok := r.Context().Value(ActorDid).(syntax.DID)
	if !ok {
		return "", false
	}
	return did.String(), true
}

// stable client-facing errors; handlers log the real cause and return these
var (
	errInternal       = xrpcErrorTag("InternalError", "Internal server error.")
	errBadRequestBody = xrpcErrorTag("InvalidRequest", "Invalid request body.")
	errUpstream       = xrpcErrorTag("UpstreamError", "An upstream service failed.")
)

func xrpcErrorTag(tag, message string) xrpcerr.XrpcError {
	return xrpcerr.NewXrpcError(xrpcerr.WithTag(tag), xrpcerr.WithMessage(message))
}

func badRequestError(message string) xrpcerr.XrpcError {
	return xrpcErrorTag("InvalidRequest", message)
}

func notFoundError(message string) xrpcerr.XrpcError {
	return xrpcErrorTag("NotFound", message)
}

func notImplementedError(message string) xrpcerr.XrpcError {
	return xrpcErrorTag("MethodNotImplemented", message)
}
