package api

import (
	"log/slog"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus/promhttp"

	"github.com/tidbcloud/alertstore/pkg/agent"
	"github.com/tidbcloud/alertstore/pkg/alert"
	"github.com/tidbcloud/alertstore/pkg/delivery"
	"github.com/tidbcloud/alertstore/pkg/directory"
	"github.com/tidbcloud/alertstore/pkg/event"
	"github.com/tidbcloud/alertstore/pkg/im"
	"github.com/tidbcloud/alertstore/pkg/jira"
	"github.com/tidbcloud/alertstore/pkg/mute"
	"github.com/tidbcloud/alertstore/pkg/preview"
	"github.com/tidbcloud/alertstore/pkg/subscription"
	"github.com/tidbcloud/alertstore/pkg/timeline"
	"github.com/tidbcloud/alertstore/pkg/webhook"
)

type Server struct {
	router             *gin.Engine
	alertService       *alert.Service
	alertStore         *alert.Store
	eventStore         *event.Store
	subStore           *subscription.Store
	muteStore          *mute.Store
	previewStore       *preview.Store
	timelineStore      *timeline.Store
	agentRuns          *agent.RunStore
	dispatcher         *delivery.Dispatcher
	im                 im.Provider
	jira               *jira.Client
	directory          *directory.Client
	webhookHandler     *webhook.Handler
	alertmanagerTokens []BearerToken
	opsPortalBaseURL   string
	logger             *slog.Logger
}

func NewServer(
	alertService *alert.Service,
	alertStore *alert.Store,
	eventStore *event.Store,
	subStore *subscription.Store,
	muteStore *mute.Store,
	previewStore *preview.Store,
	timelineStore *timeline.Store,
	agentRuns *agent.RunStore,
	dispatcher *delivery.Dispatcher,
	imProvider im.Provider,
	jiraClient *jira.Client,
	directoryClient *directory.Client,
	webhookHandler *webhook.Handler,
	alertmanagerTokens []BearerToken,
	opsPortalBaseURL string,
	logger *slog.Logger,
) *Server {
	s := &Server{
		router:             gin.Default(),
		alertService:       alertService,
		alertStore:         alertStore,
		eventStore:         eventStore,
		subStore:           subStore,
		muteStore:          muteStore,
		previewStore:       previewStore,
		timelineStore:      timelineStore,
		agentRuns:          agentRuns,
		dispatcher:         dispatcher,
		im:                 imProvider,
		jira:               jiraClient,
		directory:          directoryClient,
		webhookHandler:     webhookHandler,
		alertmanagerTokens: alertmanagerTokens,
		opsPortalBaseURL:   opsPortalBaseURL,
		logger:             logger,
	}
	s.setupRoutes()
	return s
}

func (s *Server) setupRoutes() {
	s.router.Use(corsMiddleware())

	// Browser clients are served from alerts.ops-*.pingcap.net, while the
	// protected API is published through the shared devops gateway. Keep the
	// service route identical to the public gateway route so Kong does not need
	// to rewrite paths.
	api := s.router.Group("/api/v1/alertstore")
	s.registerAPI(api)

	// BFF endpoints belong below the API prefix as well, so the shared gateway
	// ALB only needs its existing /api forwarding rule.
	s.registerBFF(api.Group("/bff"))

	// Alertmanager is also published through the shared gateway under /public.
	// Keep the complete gateway path in the service contract so the ingress can
	// use an Exact match without exposing or rewriting any other alertstore API.
	alertmanagerAuth := requireBearerToken(s.alertmanagerTokens)
	s.router.POST(
		"/public/v1/alertstore/webhooks/alertmanager",
		alertmanagerAuth,
		s.webhookAlertmanager,
	)

	// Retain the shorter route for direct, in-cluster webhook delivery. Jira is
	// private-only until its source authentication is configured.
	wh := s.router.Group("/webhooks")
	{
		wh.POST("/alertmanager", alertmanagerAuth, s.webhookAlertmanager)
		wh.POST("/jira", s.webhookJira)
	}

	// The root page redirects to the UI without using a catch-all static route,
	// which keeps health, metrics and private webhook routes distinct.
	s.router.GET("/", func(c *gin.Context) { c.Redirect(http.StatusFound, "/ui/") })
	// The image flattens the Vite build output into ./web, because the
	// environment's ConfigMap replaces ./web/runtime-config.js. Do not point
	// this at the repository's web/dist without moving that mount too.
	s.router.Static("/ui", "./web")

	// Health & metrics
	s.router.GET("/healthz", func(c *gin.Context) { c.String(200, "ok") })
	s.router.GET("/metrics", gin.WrapH(promhttp.Handler()))
}

func (s *Server) registerAPI(v1 *gin.RouterGroup) {
	{
		// Events
		v1.GET("/events", s.listEvents)
		v1.GET("/events/:id", s.getEvent)

		// Alerts
		v1.GET("/alerts", s.listAlerts)
		v1.GET("/alerts/:id", s.getAlert)
		v1.POST("/alerts/:id/ack", s.ackAlert)
		v1.POST("/alerts/:id/resolve", s.resolveAlert)
		v1.POST("/alerts/:id/assign", s.assignAlert)
		v1.POST("/alerts/:id/comment", s.commentAlert)
		v1.GET("/alerts/:id/timeline", s.alertTimeline)

		// Agent runs. The report endpoint authenticates on the per-run callback
		// token rather than on the caller's identity, so it is safe on the same
		// router as everything else: a token reaches exactly one run, and an
		// agent holds no credential that outlives its own work.
		v1.GET("/alerts/:id/agent-runs", s.listAgentRuns)
		v1.POST("/alerts/:id/agent-runs", s.createAgentRun)
		v1.PATCH("/alerts/:id/agent-runs/:runId", s.reportAgentRun)

		// Subscriptions
		v1.GET("/channels", s.listChannels)
		v1.GET("/subscriptions", s.listSubscriptions)
		v1.POST("/subscriptions", s.createSubscription)
		v1.PUT("/subscriptions/:id", s.updateSubscription)
		v1.DELETE("/subscriptions/:id", s.deleteSubscription)

		// Mute Rules
		v1.GET("/mute-rules", s.listMuteRules)
		v1.POST("/mute-rules", s.createMuteRule)
		v1.PUT("/mute-rules/:id", s.updateMuteRule)
		v1.DELETE("/mute-rules/:id", s.deleteMuteRule)

		// What a filter would have matched, for both of the forms above. POST
		// because a filter is a nested object — labels is a map — which no
		// query string states without inventing an encoding for it.
		v1.POST("/filter-preview", s.previewFilter)
	}
}

func (s *Server) registerBFF(bff *gin.RouterGroup) {
	{
		bff.GET("/alerts/counts", s.alertCounts)
		// Distinct oncall_team labels for the alert list's team filter dropdown.
		bff.GET("/alerts/teams", s.alertTeams)
		// The Dashboards page's "Now" strip: current state only, no window,
		// no team — see alertNow's doc comment for why this isn't just more
		// fields on alertCounts.
		bff.GET("/alerts/now", s.alertNow)
		// Aggregates the Dashboards page's numbers, computed fresh from the
		// alert table on every call — a UI-shaping endpoint like alertCounts
		// above, not part of the stable public API, so it lives here rather
		// than under /alerts.
		bff.GET("/alerts/stats", s.alertStats)
		bff.GET("/users", s.searchUsers)
	}
}

func (s *Server) Run(addr string) error {
	return s.router.Run(addr)
}
