package hmnurl

import (
	"fmt"
	"net/url"
	"regexp"
	"strconv"
	"strings"

	"git.handmade.network/hmn/hmn/src/logging"
	"git.handmade.network/hmn/hmn/src/models"
	"git.handmade.network/hmn/hmn/src/oops"
)

/*
Any function in this package whose name starts with Build is required to be covered by a test.
This helps ensure that we don't generate URLs that can't be routed.
*/

var RegexOldHome = regexp.MustCompile("^/home$")
var RegexHomepage = regexp.MustCompile("^/$")

func BuildHomepage() string {
	return HMNProjectContext.BuildHomepage()
}

func (c *UrlContext) BuildHomepage() string {
	return c.Url("/", nil)
}

var RegexShowcase = regexp.MustCompile("^/showcase$")

func BuildShowcase() string {
	defer CatchPanic()
	return Url("/showcase", nil)
}

var RegexStreams = regexp.MustCompile("^/streams$")

func BuildStreams() string {
	defer CatchPanic()
	return Url("/streams", nil)
}

var RegexWhenIsIt = regexp.MustCompile("^/whenisit$")

func BuildWhenIsIt() string {
	defer CatchPanic()
	return Url("/whenisit", nil)
}

var RegexJamIndex = regexp.MustCompile("^/jam$")

func BuildJamIndex() string {
	defer CatchPanic()
	return Url("/jam", nil)
}

// QUESTION(ben): Can we change these routes?

var RegexLoginAction = regexp.MustCompile("^/login$")

func BuildLoginAction(redirectTo string) string {
	defer CatchPanic()
	return Url("/login", []Q{{Name: "redirect", Value: redirectTo}})
}

var RegexLoginPage = regexp.MustCompile("^/login$")

func BuildLoginPage(redirectTo string) string {
	defer CatchPanic()
	return Url("/login", []Q{{Name: "redirect", Value: redirectTo}})
}

var RegexLogoutAction = regexp.MustCompile("^/logout$")

func BuildLogoutAction(redir string) string {
	defer CatchPanic()
	if redir == "" {
		redir = "/"
	}
	return Url("/logout", []Q{{"redirect", redir}})
}

var RegexRegister = regexp.MustCompile("^/register$")

func BuildRegister() string {
	defer CatchPanic()
	return Url("/register", nil)
}

var RegexRegistrationSuccess = regexp.MustCompile("^/registered_successfully$")

func BuildRegistrationSuccess() string {
	defer CatchPanic()
	return Url("/registered_successfully", nil)
}

var RegexEmailConfirmation = regexp.MustCompile("^/email_confirmation/(?P<username>[^/]+)/(?P<token>[^/]+)$")

func BuildEmailConfirmation(username, token string) string {
	defer CatchPanic()
	return Url(fmt.Sprintf("/email_confirmation/%s/%s", url.PathEscape(username), token), nil)
}

var RegexRequestPasswordReset = regexp.MustCompile("^/password_reset$")

func BuildRequestPasswordReset() string {
	defer CatchPanic()
	return Url("/password_reset", nil)
}

var RegexPasswordResetSent = regexp.MustCompile("^/password_reset/sent$")

func BuildPasswordResetSent() string {
	defer CatchPanic()
	return Url("/password_reset/sent", nil)
}

var RegexOldDoPasswordReset = regexp.MustCompile(`^_password_reset/(?P<username>[\w\ \.\,\-@\+\_]+)/(?P<token>[\d\w]+)[\/]?$`)
var RegexDoPasswordReset = regexp.MustCompile("^/password_reset/(?P<username>[^/]+)/(?P<token>[^/]+)$")

func BuildDoPasswordReset(username string, token string) string {
	defer CatchPanic()
	return Url(fmt.Sprintf("/password_reset/%s/%s", url.PathEscape(username), token), nil)
}

/*
* Static Pages
 */

var RegexManifesto = regexp.MustCompile("^/manifesto$")

func BuildManifesto() string {
	defer CatchPanic()
	return Url("/manifesto", nil)
}

var RegexAbout = regexp.MustCompile("^/about$")

func BuildAbout() string {
	defer CatchPanic()
	return Url("/about", nil)
}

var RegexCommunicationGuidelines = regexp.MustCompile("^/communication-guidelines$")

func BuildCommunicationGuidelines() string {
	defer CatchPanic()
	return Url("/communication-guidelines", nil)
}

var RegexContactPage = regexp.MustCompile("^/contact$")

func BuildContactPage() string {
	defer CatchPanic()
	return Url("/contact", nil)
}

var RegexMonthlyUpdatePolicy = regexp.MustCompile("^/monthly-update-policy$")

func BuildMonthlyUpdatePolicy() string {
	defer CatchPanic()
	return Url("/monthly-update-policy", nil)
}

var RegexProjectSubmissionGuidelines = regexp.MustCompile("^/project-guidelines$")

func BuildProjectSubmissionGuidelines() string {
	defer CatchPanic()
	return Url("/project-guidelines", nil)
}

/*
* User
 */

var RegexUserProfile = regexp.MustCompile(`^/m/(?P<username>[^/]+)$`)

func BuildUserProfile(username string) string {
	defer CatchPanic()
	if len(username) == 0 {
		panic(oops.New(nil, "Username must not be blank"))
	}
	return Url("/m/"+username, nil)
}

var RegexUserSettings = regexp.MustCompile(`^/settings$`)

func BuildUserSettings(section string) string {
	return UrlWithFragment("/settings", nil, section)
}

/*
* Admin
 */

var RegexAdminAtomFeed = regexp.MustCompile(`^/admin/atom$`)

func BuildAdminAtomFeed() string {
	defer CatchPanic()
	return Url("/admin/atom", nil)
}

var RegexAdminApprovalQueue = regexp.MustCompile(`^/admin/approvals$`)

func BuildAdminApprovalQueue() string {
	defer CatchPanic()
	return Url("/admin/approvals", nil)
}

var RegexAdminSetUserStatus = regexp.MustCompile(`^/admin/setuserstatus$`)

func BuildAdminSetUserStatus() string {
	defer CatchPanic()
	return Url("/admin/setuserstatus", nil)
}

var RegexAdminNukeUser = regexp.MustCompile(`^/admin/nukeuser$`)

func BuildAdminNukeUser() string {
	defer CatchPanic()
	return Url("/admin/nukeuser", nil)
}

/*
* Snippets
 */

var RegexSnippet = regexp.MustCompile(`^/snippet/(?P<snippetid>\d+)$`)

func BuildSnippet(snippetId int) string {
	defer CatchPanic()
	return Url("/snippet/"+strconv.Itoa(snippetId), nil)
}

/*
* Feed
 */

var RegexFeed = regexp.MustCompile(`^/feed(/(?P<page>.+)?)?$`)

func BuildFeed() string {
	defer CatchPanic()
	return Url("/feed", nil)
}

func BuildFeedWithPage(page int) string {
	defer CatchPanic()
	if page < 1 {
		panic(oops.New(nil, "Invalid feed page (%d), must be >= 1", page))
	}
	if page == 1 {
		return BuildFeed()
	}
	return Url("/feed/"+strconv.Itoa(page), nil)
}

var RegexAtomFeed = regexp.MustCompile("^/atom(/(?P<feedtype>[^/]+))?(/new)?$") // NOTE(asaf): `/new` for backwards compatibility with old website

func BuildAtomFeed() string {
	defer CatchPanic()
	return Url("/atom", nil)
}

func BuildAtomFeedForProjects() string {
	defer CatchPanic()
	return Url("/atom/projects", nil)
}

func BuildAtomFeedForShowcase() string {
	defer CatchPanic()
	return Url("/atom/showcase", nil)
}

/*
* Projects
 */

var RegexProjectIndex = regexp.MustCompile("^/projects(/(?P<page>.+)?)?$")

func BuildProjectIndex(page int) string {
	defer CatchPanic()
	if page < 1 {
		panic(oops.New(nil, "page must be >= 1"))
	}
	if page == 1 {
		return Url("/projects", nil)
	} else {
		return Url(fmt.Sprintf("/projects/%d", page), nil)
	}
}

var RegexProjectNew = regexp.MustCompile("^/p/new$")

func BuildProjectNew() string {
	defer CatchPanic()

	return Url("/p/new", nil)
}

var RegexPersonalProject = regexp.MustCompile("^/p/(?P<projectid>[0-9]+)(/(?P<projectslug>[a-zA-Z0-9-]+))?")

func BuildPersonalProject(id int, slug string) string {
	defer CatchPanic()
	return Url(fmt.Sprintf("/p/%d/%s", id, slug), nil)
}

var RegexProjectEdit = regexp.MustCompile("^/edit$")

func (c *UrlContext) BuildProjectEdit(section string) string {
	defer CatchPanic()

	return c.UrlWithFragment("/edit", nil, section)
}

/*
* Podcast
 */

var RegexPodcast = regexp.MustCompile(`^/podcast$`)

func BuildPodcast() string {
	defer CatchPanic()
	return Url("/podcast", nil)
}

var RegexPodcastEdit = regexp.MustCompile(`^/podcast/edit$`)

func BuildPodcastEdit() string {
	defer CatchPanic()
	return Url("/podcast/edit", nil)
}

var RegexPodcastEpisode = regexp.MustCompile(`^/podcast/ep/(?P<episodeid>[^/]+)$`)

func BuildPodcastEpisode(episodeGUID string) string {
	defer CatchPanic()
	return Url(fmt.Sprintf("/podcast/ep/%s", episodeGUID), nil)
}

var RegexPodcastEpisodeNew = regexp.MustCompile(`^/podcast/ep/new$`)

func BuildPodcastEpisodeNew() string {
	defer CatchPanic()
	return Url("/podcast/ep/new", nil)
}

var RegexPodcastEpisodeEdit = regexp.MustCompile(`^/podcast/ep/(?P<episodeid>[^/]+)/edit$`)

func BuildPodcastEpisodeEdit(episodeGUID string) string {
	defer CatchPanic()
	return Url(fmt.Sprintf("/podcast/ep/%s/edit", episodeGUID), nil)
}

var RegexPodcastRSS = regexp.MustCompile(`^/podcast/podcast.xml$`)

func BuildPodcastRSS() string {
	defer CatchPanic()
	return Url("/podcast/podcast.xml", nil)
}

func BuildPodcastEpisodeFile(filename string) string {
	defer CatchPanic()
	return BuildUserFile(fmt.Sprintf("podcast/%s/%s", models.HMNProjectSlug, filename))
}

/*
* Forums
 */

// NOTE(asaf): This also matches urls generated by BuildForumThread (/t/ is identified as a subforum, and the threadid as a page)
// Make sure to match Thread before Subforum in the router.
var RegexForum = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?(/(?P<page>\d+))?$`)

func (c *UrlContext) Url(path string, query []Q) string {
	return c.UrlWithFragment(path, query, "")
}

func (c *UrlContext) UrlWithFragment(path string, query []Q, fragment string) string {
	if c == nil {
		logging.Warn().Stack().Msg("URL context was nil; defaulting to the HMN URL context")
		c = &HMNProjectContext
	}

	if c.PersonalProject {
		url := url.URL{
			Scheme:   baseUrlParsed.Scheme,
			Host:     baseUrlParsed.Host,
			Path:     fmt.Sprintf("p/%d/%s/%s", c.ProjectID, models.GeneratePersonalProjectSlug(c.ProjectName), trim(path)),
			RawQuery: encodeQuery(query),
			Fragment: fragment,
		}

		return url.String()
	} else {
		subdomain := c.ProjectSlug
		if c.ProjectSlug == models.HMNProjectSlug {
			subdomain = ""
		}

		host := baseUrlParsed.Host
		if len(subdomain) > 0 {
			host = c.ProjectSlug + "." + host
		}

		url := url.URL{
			Scheme:   baseUrlParsed.Scheme,
			Host:     host,
			Path:     trim(path),
			RawQuery: encodeQuery(query),
			Fragment: fragment,
		}

		return url.String()
	}
}

func (c *UrlContext) BuildForum(subforums []string, page int) string {
	defer CatchPanic()
	if page < 1 {
		panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
	}

	builder := buildSubforumPath(subforums)

	if page > 1 {
		builder.WriteRune('/')
		builder.WriteString(strconv.Itoa(page))
	}

	return c.Url(builder.String(), nil)
}

var RegexForumNewThread = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/new$`)
var RegexForumNewThreadSubmit = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/new/submit$`)

func (c *UrlContext) BuildForumNewThread(subforums []string, submit bool) string {
	defer CatchPanic()
	builder := buildSubforumPath(subforums)
	builder.WriteString("/t/new")
	if submit {
		builder.WriteString("/submit")
	}

	return c.Url(builder.String(), nil)
}

var RegexForumThread = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)(-([^/]+))?(/(?P<page>\d+))?$`)

func (c *UrlContext) BuildForumThread(subforums []string, threadId int, title string, page int) string {
	defer CatchPanic()
	builder := buildForumThreadPath(subforums, threadId, title, page)

	return c.Url(builder.String(), nil)
}

func (c *UrlContext) BuildForumThreadWithPostHash(subforums []string, threadId int, title string, page int, postId int) string {
	defer CatchPanic()
	builder := buildForumThreadPath(subforums, threadId, title, page)

	return c.UrlWithFragment(builder.String(), nil, strconv.Itoa(postId))
}

var RegexForumPost = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)$`)

func (c *UrlContext) BuildForumPost(subforums []string, threadId int, postId int) string {
	defer CatchPanic()
	builder := buildForumPostPath(subforums, threadId, postId)

	return c.Url(builder.String(), nil)
}

var RegexForumPostDelete = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/delete$`)

func (c *UrlContext) BuildForumPostDelete(subforums []string, threadId int, postId int) string {
	defer CatchPanic()
	builder := buildForumPostPath(subforums, threadId, postId)
	builder.WriteString("/delete")
	return c.Url(builder.String(), nil)
}

var RegexForumPostEdit = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/edit$`)

func (c *UrlContext) BuildForumPostEdit(subforums []string, threadId int, postId int) string {
	defer CatchPanic()
	builder := buildForumPostPath(subforums, threadId, postId)
	builder.WriteString("/edit")
	return c.Url(builder.String(), nil)
}

var RegexForumPostReply = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/reply$`)

func (c *UrlContext) BuildForumPostReply(subforums []string, threadId int, postId int) string {
	defer CatchPanic()
	builder := buildForumPostPath(subforums, threadId, postId)
	builder.WriteString("/reply")
	return c.Url(builder.String(), nil)
}

var RegexWikiArticle = regexp.MustCompile(`^/wiki/(?P<threadid>\d+)(-([^/]+))?$`)

/*
* Blog
 */

var RegexBlogsRedirect = regexp.MustCompile(`^/blogs(?P<remainder>.*)`)

var RegexBlog = regexp.MustCompile(`^/blog(/(?P<page>\d+))?$`)

func (c *UrlContext) BuildBlog(page int) string {
	defer CatchPanic()
	if page < 1 {
		panic(oops.New(nil, "Invalid blog page (%d), must be >= 1", page))
	}
	path := "/blog"

	if page > 1 {
		path += "/" + strconv.Itoa(page)
	}

	return c.Url(path, nil)
}

var RegexBlogThread = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)(-([^/]+))?$`)

func (c *UrlContext) BuildBlogThread(threadId int, title string) string {
	defer CatchPanic()
	builder := buildBlogThreadPath(threadId, title)
	return c.Url(builder.String(), nil)
}

func (c *UrlContext) BuildBlogThreadWithPostHash(threadId int, title string, postId int) string {
	defer CatchPanic()
	builder := buildBlogThreadPath(threadId, title)
	return c.UrlWithFragment(builder.String(), nil, strconv.Itoa(postId))
}

var RegexBlogNewThread = regexp.MustCompile(`^/blog/new$`)

func (c *UrlContext) BuildBlogNewThread() string {
	defer CatchPanic()
	return c.Url("/blog/new", nil)
}

var RegexBlogPost = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)$`)

func (c *UrlContext) BuildBlogPost(threadId int, postId int) string {
	defer CatchPanic()
	builder := buildBlogPostPath(threadId, postId)
	return c.Url(builder.String(), nil)
}

var RegexBlogPostDelete = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)/delete$`)

func (c *UrlContext) BuildBlogPostDelete(threadId int, postId int) string {
	defer CatchPanic()
	builder := buildBlogPostPath(threadId, postId)
	builder.WriteString("/delete")
	return c.Url(builder.String(), nil)
}

var RegexBlogPostEdit = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)/edit$`)

func (c *UrlContext) BuildBlogPostEdit(threadId int, postId int) string {
	defer CatchPanic()
	builder := buildBlogPostPath(threadId, postId)
	builder.WriteString("/edit")
	return c.Url(builder.String(), nil)
}

var RegexBlogPostReply = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)/reply$`)

func (c *UrlContext) BuildBlogPostReply(threadId int, postId int) string {
	defer CatchPanic()
	builder := buildBlogPostPath(threadId, postId)
	builder.WriteString("/reply")
	return c.Url(builder.String(), nil)
}

/*
* Library
 */

// Any library route. Remove after we port the library.
var RegexLibraryAny = regexp.MustCompile(`^/library`)

var RegexLibrary = regexp.MustCompile(`^/library$`)

func BuildLibrary() string {
	defer CatchPanic()
	return Url("/library", nil)
}

var RegexLibraryAll = regexp.MustCompile(`^/library/all$`)

func BuildLibraryAll() string {
	defer CatchPanic()
	return Url("/library/all", nil)
}

var RegexLibraryTopic = regexp.MustCompile(`^/library/topic/(?P<topicid>\d+)$`)

func BuildLibraryTopic(topicId int) string {
	defer CatchPanic()
	if topicId < 1 {
		panic(oops.New(nil, "Invalid library topic ID (%d), must be >= 1", topicId))
	}

	var builder strings.Builder
	builder.WriteString("/library/topic/")
	builder.WriteString(strconv.Itoa(topicId))

	return Url(builder.String(), nil)
}

var RegexLibraryResource = regexp.MustCompile(`^/library/resource/(?P<resourceid>\d+)$`)

func BuildLibraryResource(resourceId int) string {
	defer CatchPanic()
	builder := buildLibraryResourcePath(resourceId)

	return Url(builder.String(), nil)
}

/*
* Episode Guide
 */

var RegexEpisodeList = regexp.MustCompile(`^/episode(/(?P<topic>[^/]+))?$`)

func (c *UrlContext) BuildEpisodeList(topic string) string {
	defer CatchPanic()

	var builder strings.Builder
	builder.WriteString("/episode")
	if topic != "" {
		builder.WriteString("/")
		builder.WriteString(topic)
	}
	return c.Url(builder.String(), nil)
}

var RegexEpisode = regexp.MustCompile(`^/episode/(?P<topic>[^/]+)/(?P<episode>[^/]+)$`)

func (c *UrlContext) BuildEpisode(topic string, episode string) string {
	defer CatchPanic()
	return c.Url(fmt.Sprintf("/episode/%s/%s", topic, episode), nil)
}

var RegexCineraIndex = regexp.MustCompile(`^/(?P<topic>[^/]+).index$`)

func (c *UrlContext) BuildCineraIndex(topic string) string {
	defer CatchPanic()
	return c.Url(fmt.Sprintf("/%s.index", topic), nil)
}

/*
* Discord OAuth
 */

var RegexDiscordOAuthCallback = regexp.MustCompile("^/_discord_callback$")

func BuildDiscordOAuthCallback() string {
	return Url("/_discord_callback", nil)
}

var RegexDiscordUnlink = regexp.MustCompile("^/_discord_unlink$")

func BuildDiscordUnlink() string {
	return Url("/_discord_unlink", nil)
}

var RegexDiscordShowcaseBacklog = regexp.MustCompile("^/discord_showcase_backlog$")

func BuildDiscordShowcaseBacklog() string {
	return Url("/discord_showcase_backlog", nil)
}

/*
* API
 */

var RegexAPICheckUsername = regexp.MustCompile("^/api/check_username$")

func BuildAPICheckUsername() string {
	return Url("/api/check_username", nil)
}

/*
* Twitch stuff
 */

var RegexTwitchEventSubCallback = regexp.MustCompile("^/twitch_eventsub$")

func BuildTwitchEventSubCallback() string {
	return Url("/twitch_eventsub", nil)
}

var RegexTwitchDebugPage = regexp.MustCompile("^/twitch_debug$")

/*
* User assets
 */

var RegexAssetUpload = regexp.MustCompile("^/upload_asset$")

// NOTE(asaf): Providing the projectSlug avoids any CORS problems.
func (c *UrlContext) BuildAssetUpload() string {
	return c.Url("/upload_asset", nil)
}

/*
* Assets
 */

var RegexProjectCSS = regexp.MustCompile("^/assets/project.css$")

func BuildProjectCSS(color string) string {
	defer CatchPanic()
	return Url("/assets/project.css", []Q{{"color", color}})
}

var RegexMarkdownWorkerJS = regexp.MustCompile("^/assets/markdown_worker.js$")

func BuildMarkdownWorkerJS() string {
	defer CatchPanic()
	return Url("/assets/markdown_worker.js", nil)
}

var RegexS3Asset *regexp.Regexp

func BuildS3Asset(s3key string) string {
	defer CatchPanic()
	res := fmt.Sprintf("%s%s", S3BaseUrl, s3key)
	return res
}

var RegexPublic = regexp.MustCompile("^/public/.+$")

func BuildPublic(filepath string, cachebust bool) string {
	defer CatchPanic()
	filepath = strings.Trim(filepath, "/")
	if len(strings.TrimSpace(filepath)) == 0 {
		panic(oops.New(nil, "Attempted to build a /public url with no path"))
	}
	if strings.Contains(filepath, "?") {
		panic(oops.New(nil, "Public url failpath must not contain query params"))
	}
	var builder strings.Builder
	builder.WriteString("/public")
	pathParts := strings.Split(filepath, "/")
	for _, part := range pathParts {
		part = strings.TrimSpace(part)
		if len(part) == 0 {
			panic(oops.New(nil, "Attempted to build a /public url with blank path segments: %s", filepath))
		}
		builder.WriteRune('/')
		builder.WriteString(part)
	}
	var query []Q
	if cachebust {
		query = []Q{{"v", cacheBust}}
	}
	return Url(builder.String(), query)
}

func BuildTheme(filepath string, theme string, cachebust bool) string {
	defer CatchPanic()
	filepath = strings.Trim(filepath, "/")
	if len(theme) == 0 {
		panic(oops.New(nil, "Theme can't be blank"))
	}
	return BuildPublic(fmt.Sprintf("themes/%s/%s", theme, filepath), cachebust)
}

func BuildUserFile(filepath string) string {
	if filepath == "" {
		return ""
	}

	filepath = strings.Trim(filepath, "/")
	return BuildPublic(fmt.Sprintf("media/%s", filepath), false)
}

/*
* Other
 */

var RegexForumMarkRead = regexp.MustCompile(`^/markread/(?P<sfid>\d+)$`)

// NOTE(asaf): subforumId == 0 means ALL SUBFORUMS
func (c *UrlContext) BuildForumMarkRead(subforumId int) string {
	defer CatchPanic()
	if subforumId < 0 {
		panic(oops.New(nil, "Invalid subforum ID (%d), must be >= 0", subforumId))
	}

	var builder strings.Builder
	builder.WriteString("/markread/")
	builder.WriteString(strconv.Itoa(subforumId))

	return c.Url(builder.String(), nil)
}

var RegexCatchAll = regexp.MustCompile("^")

/*
* Helper functions
 */

func buildSubforumPath(subforums []string) *strings.Builder {
	for _, subforum := range subforums {
		if strings.Contains(subforum, "/") {
			panic(oops.New(nil, "Tried building forum url with / in subforum name"))
		}
		subforum = strings.TrimSpace(subforum)
		if len(subforum) == 0 {
			panic(oops.New(nil, "Tried building forum url with blank subforum"))
		}
	}

	var builder strings.Builder
	builder.WriteString("/forums")
	for _, subforum := range subforums {
		builder.WriteRune('/')
		builder.WriteString(subforum)
	}

	return &builder
}

func buildForumThreadPath(subforums []string, threadId int, title string, page int) *strings.Builder {
	if page < 1 {
		panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
	}

	if threadId < 1 {
		panic(oops.New(nil, "Invalid forum thread ID (%d), must be >= 1", threadId))
	}

	builder := buildSubforumPath(subforums)

	builder.WriteString("/t/")
	builder.WriteString(strconv.Itoa(threadId))
	if len(title) > 0 {
		builder.WriteRune('-')
		builder.WriteString(PathSafeTitle(title))
	}
	if page > 1 {
		builder.WriteRune('/')
		builder.WriteString(strconv.Itoa(page))
	}

	return builder
}

func buildForumPostPath(subforums []string, threadId int, postId int) *strings.Builder {
	if threadId < 1 {
		panic(oops.New(nil, "Invalid forum thread ID (%d), must be >= 1", threadId))
	}

	if postId < 1 {
		panic(oops.New(nil, "Invalid forum post ID (%d), must be >= 1", postId))
	}

	builder := buildSubforumPath(subforums)

	builder.WriteString("/t/")
	builder.WriteString(strconv.Itoa(threadId))
	builder.WriteString("/p/")
	builder.WriteString(strconv.Itoa(postId))

	return builder
}

func buildBlogThreadPath(threadId int, title string) *strings.Builder {
	if threadId < 1 {
		panic(oops.New(nil, "Invalid blog thread ID (%d), must be >= 1", threadId))
	}

	var builder strings.Builder

	builder.WriteString("/blog/p/")
	builder.WriteString(strconv.Itoa(threadId))
	if len(title) > 0 {
		builder.WriteRune('-')
		builder.WriteString(PathSafeTitle(title))
	}

	return &builder
}

func buildBlogPostPath(threadId int, postId int) *strings.Builder {
	if threadId < 1 {
		panic(oops.New(nil, "Invalid blog thread ID (%d), must be >= 1", threadId))
	}

	if postId < 1 {
		panic(oops.New(nil, "Invalid blog post ID (%d), must be >= 1", postId))
	}

	var builder strings.Builder

	builder.WriteString("/blog/p/")
	builder.WriteString(strconv.Itoa(threadId))
	builder.WriteString("/e/")
	builder.WriteString(strconv.Itoa(postId))

	return &builder
}

func buildLibraryResourcePath(resourceId int) *strings.Builder {
	if resourceId < 1 {
		panic(oops.New(nil, "Invalid library resource ID (%d), must be >= 1", resourceId))
	}

	var builder strings.Builder
	builder.WriteString("/library/resource/")
	builder.WriteString(strconv.Itoa(resourceId))

	return &builder
}

func buildLibraryDiscussionPath(resourceId int, threadId int, page int) *strings.Builder {
	if page < 1 {
		panic(oops.New(nil, "Invalid page number (%d), must be >= 1", page))
	}
	if threadId < 1 {
		panic(oops.New(nil, "Invalid library thread ID (%d), must be >= 1", threadId))
	}
	builder := buildLibraryResourcePath(resourceId)
	builder.WriteString("/d/")
	builder.WriteString(strconv.Itoa(threadId))
	if page > 1 {
		builder.WriteRune('/')
		builder.WriteString(strconv.Itoa(page))
	}
	return builder
}

func buildLibraryPostPath(resourceId int, threadId int, postId int) *strings.Builder {
	if threadId < 1 {
		panic(oops.New(nil, "Invalid library thread ID (%d), must be >= 1", threadId))
	}
	if postId < 1 {
		panic(oops.New(nil, "Invalid library post ID (%d), must be >= 1", postId))
	}
	builder := buildLibraryResourcePath(resourceId)
	builder.WriteString("/d/")
	builder.WriteString(strconv.Itoa(threadId))
	builder.WriteString("/p/")
	builder.WriteString(strconv.Itoa(postId))
	return builder
}

var PathCharsToClear = regexp.MustCompile("[$&`<>{}()\\[\\]\"+#%@;=?\\\\^|~‘]")
var PathCharsToReplace = regexp.MustCompile("[ :/\\\\]")

func PathSafeTitle(title string) string {
	title = strings.ToLower(title)
	title = PathCharsToReplace.ReplaceAllLiteralString(title, "_")
	title = PathCharsToClear.ReplaceAllLiteralString(title, "")
	title = url.PathEscape(title)
	return title
}

// TODO(asaf): Find a nicer solution that doesn't require adding a defer to every construction function while also not printing errors in tests.
func CatchPanic() {
	if !isTest {
		if recovered := recover(); recovered != nil {
			logging.LogPanicValue(nil, recovered, "Url construction failed")
		}
	}
}
