package state

import (
	"context"
	"database/sql"
	"errors"
	"net/http"
	"strings"

	"github.com/go-chi/chi/v5"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/focus"
	"tangled.org/core/appview/issues"
	"tangled.org/core/appview/knotacl"
	"tangled.org/core/appview/knots"
	"tangled.org/core/appview/labels"
	"tangled.org/core/appview/metrics"
	"tangled.org/core/appview/middleware"
	"tangled.org/core/appview/migration"
	"tangled.org/core/appview/notifications"
	whnotify "tangled.org/core/appview/notify/webhook"
	"tangled.org/core/appview/pipelines"
	"tangled.org/core/appview/pulls"
	"tangled.org/core/appview/repo"
	"tangled.org/core/appview/settings"
	"tangled.org/core/appview/signup"
	"tangled.org/core/appview/spindles"
	"tangled.org/core/appview/state/userutil"
	avstrings "tangled.org/core/appview/strings"
	avtimeline "tangled.org/core/appview/timeline"
	avxrpc "tangled.org/core/appview/xrpc"
	"tangled.org/core/blog"
	"tangled.org/core/log"
	"tangled.org/core/xrpc/serviceauth"
)

func (s *State) Router() http.Handler {
	router := chi.NewRouter()
	middleware := middleware.New(
		s.oauth,
		s.db,
		s.enforcer,
		s.aclService,
		s.repoResolver,
		s.idResolver,
		s.pages,
		s.rdb,
		s.logger,
	)

	router.Use(metrics.Middleware)
	router.Use(knotacl.MemoMiddleware)

	if err := db.ReapStaleRunningMigrations(context.Background(), s.db); err != nil {
		s.logger.Warn("failed to reap stale running migrations", "err", err)
	}
	go func() {
		if n, err := db.EnqueueEntityStateBackfill(context.Background(), s.db); err != nil {
			s.logger.Warn("failed to enqueue entity state backfill", "err", err)
		} else if n > 0 {
			s.logger.Info("enqueued entity state backfill", "owners", n)
		}
	}()
	m := migration.NewMigration(s.db, s.oauth, s.idResolver.Directory(), s.logger)
	router.Use(m.BackgroundMigrationMiddleware)

	router.Get("/pwa-manifest.json", s.WebAppManifest)
	router.Get("/robots.txt", s.RobotsTxt)
	router.Get("/.well-known/security.txt", s.SecurityTxt)

	userRouter := s.UserRouter(&middleware)
	standardRouter := s.StandardRouter(&middleware)
	keysRouter := s.KeysRouter(&middleware)

	router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
		pat := chi.URLParam(r, "*")
		pathParts := strings.SplitN(pat, "/", 2)

		if len(pathParts) > 0 {
			firstPart := pathParts[0]

			if userutil.IsDid(firstPart) {
				repo, err := db.GetRepoByDid(s.db, firstPart)
				switch {
				case err == nil:
					remaining := ""
					if len(pathParts) > 1 {
						remaining = "/" + pathParts[1]
					}
					rewritten := "/" + repo.Did + "/" + repo.Rkey + remaining
					r2 := r.Clone(r.Context())
					r2.URL.Path = rewritten
					r2.URL.RawPath = rewritten
					userRouter.ServeHTTP(w, r2)
				case errors.Is(err, sql.ErrNoRows):
					userRouter.ServeHTTP(w, r)
				default:
					s.logger.Error("db error looking up repo DID", "repoDid", firstPart, "err", err)
					http.Error(w, "internal server error", http.StatusInternalServerError)
				}
				return
			}

			if strings.HasSuffix(firstPart, ".keys") {
				keysRouter.ServeHTTP(w, r)
				return
			}

			if userutil.IsHandle(firstPart) {
				userRouter.ServeHTTP(w, r)
				return
			}

			// if using a flattened DID (like you would in go modules), unflatten
			if userutil.IsFlattenedDid(firstPart) {
				unflattenedDid := userutil.UnflattenDid(firstPart)
				redirectPath := strings.Join(append([]string{unflattenedDid}, pathParts[1:]...), "/")

				redirectURL := *r.URL
				redirectURL.Path = "/" + redirectPath

				http.Redirect(w, r, redirectURL.String(), http.StatusFound)
				return
			}

			// if using a handle with @, rewrite to work without @
			if normalized := strings.TrimPrefix(firstPart, "@"); userutil.IsHandle(normalized) {
				redirectPath := strings.Join(append([]string{normalized}, pathParts[1:]...), "/")

				redirectURL := *r.URL
				redirectURL.Path = "/" + redirectPath

				http.Redirect(w, r, redirectURL.String(), http.StatusFound)
				return
			}

		}

		standardRouter.ServeHTTP(w, r)
	})

	return router
}

func (s *State) KeysRouter(mw *middleware.Middleware) http.Handler {
	r := chi.NewRouter()
	r.Use(mw.InjectBaseParams)

	r.With(mw.ResolveIdent()).Route("/{user}", func(r chi.Router) {
		r.Get("/", s.Keys)
	})

	return r
}

func (s *State) UserRouter(mw *middleware.Middleware) http.Handler {
	r := chi.NewRouter()
	r.Use(mw.InjectBaseParams)

	// TODO: workflow status update requests (30s polling)

	r.With(mw.ResolveIdent()).Route("/{user}", func(r chi.Router) {
		r.Get("/", s.Profile)
		r.Get("/feed.atom", s.AtomFeedPage)

		r.With(mw.ResolveRepo()).Route("/{repo}", func(r chi.Router) {
			r.Use(mw.GoImport())

			// These routes get proxied to the knot
			r.Get("/info/refs", s.InfoRefs)
			r.Post("/git-upload-archive", s.UploadArchive)
			r.Post("/git-upload-pack", s.UploadPack)
			r.Post("/git-receive-pack", s.ReceivePack)

			r.Group(func(r chi.Router) {
				r.Use(mw.CanonicalizeRepoURL())
				r.Mount("/issues", s.IssuesRouter(mw))
				r.Mount("/pulls", s.PullsRouter(mw))
				r.Mount("/pipelines", s.PipelinesRouter(mw))
				r.Mount("/labels", s.LabelsRouter())
				r.Mount("/", s.RepoRouter(mw))
			})
		})
	})

	r.NotFound(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNotFound)
		s.pages.Error404(w)
	})

	return r
}

func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler {
	r := chi.NewRouter()
	r.Use(mw.InjectBaseParams)

	r.Handle("/static/*", s.pages.Static())

	tl := avtimeline.New(s.oauth, s.db, s.config, s.pages, s.logger, blog.PostsFS)
	r.Get("/", tl.HomeOrTimeline)
	r.Get("/home", tl.Home)
	r.Get("/timeline", tl.Timeline)
	r.Get("/upgradeBanner", s.UpgradeBanner)
	r.Post("/newsletter/signup", s.NewsletterSignup)
	r.Post("/newsletter/dismiss", s.NewsletterDismiss)

	// special-case handler for serving tangled.org/core
	r.Get("/core", s.Core())

	r.Get("/login", s.Login)
	r.Post("/login", s.Login)
	r.Post("/logout", s.Logout)

	r.With(middleware.Paginate).Get("/search", s.Search)
	r.With(middleware.AuthMiddleware(s.oauth)).Get("/search/quick", s.SearchQuick)
	r.With(middleware.AuthMiddleware(s.oauth)).Get("/search/quick/mobile", s.SearchQuickMobile)

	r.Post("/account/switch", s.SwitchAccount)
	r.With(middleware.AuthMiddleware(s.oauth)).Delete("/account/{did}", s.RemoveAccount)

	r.Route("/repo", func(r chi.Router) {
		r.Route("/new", func(r chi.Router) {
			r.Use(middleware.AuthMiddleware(s.oauth))
			r.Get("/", s.NewRepo)
			r.Post("/", s.NewRepo)
		})
		// r.Post("/import", s.ImportRepo)
	})

	r.With(middleware.Paginate).Get("/goodfirstissues", s.GoodFirstIssues)

	r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) {
		r.Post("/", s.Follow)
		r.Delete("/", s.Follow)
	})

	r.With(middleware.AuthMiddleware(s.oauth)).Route("/vouch", func(r chi.Router) {
		r.Post("/", s.Vouch)
		r.Post("/skip", s.SkipVouchSuggestion)
	})

	r.With(middleware.AuthMiddleware(s.oauth)).Route("/star", func(r chi.Router) {
		r.Post("/", s.Star)
		r.Delete("/", s.Star)
	})

	r.With(middleware.AuthMiddleware(s.oauth)).Route("/react", func(r chi.Router) {
		r.Post("/", s.React)
		r.Delete("/", s.React)
	})

	r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) {
		r.Get("/", s.CommentBodyFragment)
		r.Get("/edit", s.EditCommentFragment)
		r.Get("/reply", s.NewReplyCommentFragment)
		r.Get("/reply/placeholder", s.ReplyPlaceholderFragment)
		r.Post("/", s.NewComment)
		r.Patch("/", s.EditComment)
		r.Delete("/", s.DeleteComment)
	})

	r.With(middleware.AuthMiddleware(s.oauth)).Route("/markup", func(r chi.Router) {
		r.Post("/preview", s.MarkdownPreview)
		r.Post("/upload", s.MarkupUpload)
	})
	r.Get("/profile/popover", s.ProfilePopover)

	r.Route("/profile", func(r chi.Router) {
		r.Use(middleware.AuthMiddleware(s.oauth))
		r.Get("/edit-bio", s.EditBioFragment)
		r.Get("/edit-pins", s.EditPinsFragment)
		r.Post("/bio", s.UpdateProfileBio)
		r.Post("/pins", s.UpdateProfilePins)
		r.Post("/avatar", s.UploadProfileAvatar)
		r.Delete("/avatar", s.RemoveProfileAvatar)
		r.Post("/punchcard", s.UpdateProfilePunchcardSetting)
		r.Post("/theme", s.UpdateProfileThemeSetting)
	})

	r.With(middleware.AuthMiddleware(s.oauth)).Route("/welcome", func(r chi.Router) {
		r.Get("/", s.OnboardingResume)
		r.Get("/profile", s.OnboardingProfile)
		r.Post("/profile", s.OnboardingSaveProfile)
		r.Get("/keys", s.OnboardingKeys)
		r.Get("/social", s.OnboardingSocial)
		r.Get("/repo", s.OnboardingRepo)
		r.Post("/next", s.OnboardingNext)
		r.Post("/skip", s.OnboardingSkip)
		r.Post("/complete", s.OnboardingComplete)
	})

	r.Mount("/settings", s.SettingsRouter())
	r.Mount("/strings", s.StringsRouter(mw))

	r.Mount("/settings/knots", s.KnotsRouter())
	r.Mount("/settings/spindles", s.SpindlesRouter())

	r.Mount("/notifications", s.NotificationsRouter(mw))
	r.Mount("/focus", s.FocusRouter(mw))

	r.Mount("/signup", s.SignupRouter())
	if s.config.Core.XrpcEnabled {
		r.Mount("/xrpc", s.XrpcRouter())
	}
	r.Mount("/", s.oauth.Router())

	r.Get("/terms", s.TermsOfService)
	r.Get("/privacy", s.PrivacyPolicy)
	r.Get("/brand", s.Brand)

	r.NotFound(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNotFound)
		s.pages.Error404(w)
	})
	return r
}

// Core serves tangled.org/core go-import meta tags, and redirects
// to the core repository if accessed normally.
func (s *State) Core() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("go-get") == "1" {
			w.Header().Set("Content-Type", "text/html")
			w.Write([]byte(`<meta name="go-import" content="tangled.org/core git https://tangled.org/@tangled.org/core">`))
			return
		}

		http.Redirect(w, r, "/@tangled.org/core", http.StatusFound)
	}
}

func (s *State) SettingsRouter() http.Handler {
	settings := &settings.Settings{
		Db:         s.db,
		OAuth:      s.oauth,
		Pages:      s.pages,
		Config:     s.config,
		CfClient:   s.cfClient,
		Logger:     log.SubLogger(s.logger, "settings"),
		IdResolver: s.idResolver,
	}

	return settings.Router()
}

func (s *State) SpindlesRouter() http.Handler {
	logger := log.SubLogger(s.logger, "spindles")

	spindles := &spindles.Spindles{
		Db:         s.db,
		OAuth:      s.oauth,
		Pages:      s.pages,
		Config:     s.config,
		Enforcer:   s.enforcer,
		IdResolver: s.idResolver,
		Logger:     logger,
	}

	return spindles.Router()
}

func (s *State) KnotsRouter() http.Handler {
	logger := log.SubLogger(s.logger, "knots")

	knots := &knots.Knots{
		Db:         s.db,
		OAuth:      s.oauth,
		Pages:      s.pages,
		Config:     s.config,
		Enforcer:   s.enforcer,
		Acl:        s.aclService,
		IdResolver: s.idResolver,
		Knotstream: s.knotstream,
		Logger:     logger,
	}

	return knots.Router()
}

func (s *State) StringsRouter(mw *middleware.Middleware) http.Handler {
	logger := log.SubLogger(s.logger, "strings")

	strs := &avstrings.Strings{
		Db:         s.db,
		OAuth:      s.oauth,
		Pages:      s.pages,
		IdResolver: s.idResolver,
		Notifier:   s.notifier,
		Logger:     logger,
	}

	return strs.Router(mw)
}

func (s *State) IssuesRouter(mw *middleware.Middleware) http.Handler {
	issues := issues.New(
		s.oauth,
		s.repoResolver,
		s.aclService,
		s.pages,
		s.idResolver,
		s.mentionsResolver,
		s.db,
		s.config,
		s.notifier,
		s.indexer.Issues,
		log.SubLogger(s.logger, "issues"),
	)
	return issues.Router(mw)
}

func (s *State) PullsRouter(mw *middleware.Middleware) http.Handler {
	pulls := pulls.New(
		s.oauth,
		s.repoResolver,
		s.pages,
		s.idResolver,
		s.mentionsResolver,
		s.db,
		s.config,
		s.notifier,
		s.aclService,
		s.indexer.Pulls,
		log.SubLogger(s.logger, "pulls"),
	)
	return pulls.Router(mw)
}

func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler {
	repo := repo.New(
		s.oauth,
		s.repoResolver,
		s.pages,
		s.idResolver,
		s.db,
		s.config,
		s.notifier,
		s.enforcer,
		s.aclService,
		log.SubLogger(s.logger, "repo"),
		s.cfClient,
		s.codesearch,
	)
	return repo.Router(mw)
}

func (s *State) PipelinesRouter(mw *middleware.Middleware) http.Handler {
	pipes := pipelines.New(
		s.oauth,
		s.repoResolver,
		s.pages,
		s.idResolver,
		s.db,
		s.config,
		s.enforcer,
		log.SubLogger(s.logger, "pipelines"),
	)
	return pipes.Router(mw)
}

func (s *State) LabelsRouter() http.Handler {
	ls := labels.New(
		s.oauth,
		s.pages,
		s.db,
		s.idResolver.Directory(),
		s.aclService,
		s.notifier,
		log.SubLogger(s.logger, "labels"),
	)
	return ls.Router()
}

func (s *State) NotificationsRouter(mw *middleware.Middleware) http.Handler {
	notifs := notifications.New(s.db, s.oauth, s.pages, log.SubLogger(s.logger, "notifications"))
	return notifs.Router(mw)
}

func (s *State) FocusRouter(mw *middleware.Middleware) http.Handler {
	f := focus.New(s.db, s.oauth, s.idResolver, s.pages, log.SubLogger(s.logger, "focus"))
	return f.Router(mw)
}

func (s *State) SignupRouter() http.Handler {
	sig := signup.New(s.config, s.db, s.posthog, s.idResolver, s.pages, log.SubLogger(s.logger, "signup"))
	return sig.Router()
}

// XrpcRouter serves the org.tangled.* methods owned by the go service; callers
// authenticate with atproto service auth, audience did:web:<appview host>
func (s *State) XrpcRouter() http.Handler {
	audience := serviceauth.DidWeb(s.config.Core.AppviewHost).String()
	sa := serviceauth.NewServiceAuth(s.logger, s.idResolver.Directory(), audience)

	xlogger := log.SubLogger(s.logger, "xrpc")
	x := &avxrpc.Xrpc{
		DB:                  s.db,
		Config:              s.config,
		Logger:              xlogger,
		ServiceAuth:         sa,
		IdResolver:          s.idResolver,
		Cloudflare:          s.cfClient,
		CodeSearch:          s.codesearch,
		Webhooks:            whnotify.NewNotifier(s.db, s.config.Core.BaseUrl(), s.config.Core.Dev),
		DisallowedNicknames: userutil.LoadDisallowedNicknames(s.config.Core.DisallowedNicknamesFile, xlogger),
	}
	return x.Router()
}
