package website

import (
	"bytes"
	"errors"
	"fmt"
	"image"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"

	"git.handmade.network/hmn/hmn/src/assets"
	"git.handmade.network/hmn/hmn/src/db"
	"git.handmade.network/hmn/hmn/src/embed"
	"git.handmade.network/hmn/hmn/src/hmndata"
	"git.handmade.network/hmn/hmn/src/hmnurl"
	"git.handmade.network/hmn/hmn/src/models"
	"git.handmade.network/hmn/hmn/src/oops"
	"git.handmade.network/hmn/hmn/src/parsing"
	"git.handmade.network/hmn/hmn/src/templates"
	"github.com/google/uuid"

	"mvdan.cc/xurls/v2"
)

type SnippetData struct {
	templates.BaseData
	Snippet templates.TimelineItem

	CanEditSnippet bool
	SnippetEdit    templates.SnippetEdit
}

func Snippet(c *RequestContext) ResponseData {
	snippetId := -1
	snippetIdStr, found := c.PathParams["snippetid"]
	if found && snippetIdStr != "" {
		var err error
		if snippetId, err = strconv.Atoi(snippetIdStr); err != nil {
			return FourOhFour(c)
		}
	}
	if snippetId < 1 {
		return FourOhFour(c)
	}

	s, err := hmndata.FetchSnippet(c, c.Conn, c.CurrentUser, snippetId, hmndata.SnippetQuery{})
	if err != nil {
		if errors.Is(err, db.NotFound) {
			return FourOhFour(c)
		} else {
			return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippet"))
		}
	}
	c.Perf.EndBlock()

	canEdit := (c.CurrentUser != nil && (c.CurrentUser.IsStaff || c.CurrentUser.ID == s.Owner.ID))
	snippet := SnippetToTimelineItem(&s.Snippet, s.Asset, s.DiscordMessage, s.Projects, s.Owner, c.Theme, canEdit)
	snippet.SmallInfo = true

	opengraph := []templates.OpenGraphItem{
		{Property: "og:site_name", Value: "Handmade Network"},
		{Property: "og:type", Value: "article"},
		{Property: "og:url", Value: snippet.Url},
		{Property: "og:title", Value: fmt.Sprintf("Snippet by %s", snippet.OwnerName)},
		{Property: "og:description", Value: string(snippet.Description)},
	}

	if len(snippet.EmbedMedia) > 0 {
		media := snippet.EmbedMedia[0]

		switch media.Type {
		case templates.TimelineItemMediaTypeImage:
			opengraph = append(opengraph,
				templates.OpenGraphItem{Property: "og:image", Value: media.AssetUrl},
				templates.OpenGraphItem{Property: "og:image:width", Value: strconv.Itoa(media.Width)},
				templates.OpenGraphItem{Property: "og:image:height", Value: strconv.Itoa(media.Height)},
				templates.OpenGraphItem{Property: "og:image:type", Value: media.MimeType},
				templates.OpenGraphItem{Name: "twitter:card", Value: "summary_large_image"},
			)
		case templates.TimelineItemMediaTypeVideo:
			opengraph = append(opengraph,
				templates.OpenGraphItem{Property: "og:video", Value: media.AssetUrl},
				templates.OpenGraphItem{Property: "og:video:width", Value: strconv.Itoa(media.Width)},
				templates.OpenGraphItem{Property: "og:video:height", Value: strconv.Itoa(media.Height)},
				templates.OpenGraphItem{Property: "og:video:type", Value: media.MimeType},
				templates.OpenGraphItem{Name: "twitter:card", Value: "player"},
			)
		case templates.TimelineItemMediaTypeAudio:
			opengraph = append(opengraph,
				templates.OpenGraphItem{Property: "og:audio", Value: media.AssetUrl},
				templates.OpenGraphItem{Property: "og:audio:type", Value: media.MimeType},
				templates.OpenGraphItem{Name: "twitter:card", Value: "player"},
			)
		}
		opengraph = append(opengraph, media.ExtraOpenGraphItems...)
	}

	baseData := getBaseData(
		c,
		fmt.Sprintf("Snippet by %s", snippet.OwnerName),
		[]templates.Breadcrumb{{Name: snippet.OwnerName, Url: snippet.OwnerUrl}},
	)
	baseData.OpenGraphItems = opengraph // NOTE(asaf): We're overriding the defaults on purpose.
	snippetEdit := templates.SnippetEdit{}
	if c.CurrentUser != nil {
		userProjects, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
			OwnerIDs: []int{c.CurrentUser.ID},
		})
		if err != nil {
			return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user projects"))
		}
		templateProjects := make([]templates.Project, 0, len(userProjects))
		for _, p := range userProjects {
			templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage(), c.Theme)
			templateProjects = append(templateProjects, templateProject)
		}
		snippetEdit = templates.SnippetEdit{
			AvailableProjectsJSON: templates.SnippetEditProjectsToJSON(templateProjects),
			SubmitUrl:             hmnurl.BuildSnippetSubmit(),
			AssetMaxSize:          AssetMaxSize(c.CurrentUser),
		}
	}
	var res ResponseData
	err = res.WriteTemplate("snippet.html", SnippetData{
		BaseData:       baseData,
		Snippet:        snippet,
		CanEditSnippet: canEdit,
		SnippetEdit:    snippetEdit,
	}, c.Perf)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render snippet template"))
	}
	return res
}

func SnippetEditSubmit(c *RequestContext) ResponseData {
	maxUploadSize := AssetMaxSize(c.CurrentUser)
	maxBodySize := int64(maxUploadSize + 1024*1024)
	c.Req.Body = http.MaxBytesReader(c.Res, c.Req.Body, maxBodySize)
	err := c.Req.ParseMultipartForm(maxBodySize)
	if err != nil {
		// NOTE(asaf): The error for exceeding the max filesize doesn't have a special type, so we can't easily detect it here.
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to parse form"))
	}

	form := c.Req.PostForm

	redirect := form.Get("redirect")
	action := form.Get("action")
	existingSnippetIdStr := strings.TrimSpace(form.Get("snippet_id"))
	var existingSnippet *hmndata.SnippetAndStuff
	originalText := ""
	var embedUrl *string
	var assetID *uuid.UUID

	if len(existingSnippetIdStr) > 0 {
		existingSnippetId, err := strconv.Atoi(existingSnippetIdStr)
		if err != nil {
			return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to parse snippet id"))
		}
		query := hmndata.SnippetQuery{}
		if !c.CurrentUser.IsStaff {
			query.OwnerIDs = []int{c.CurrentUser.ID}
		}
		snip, err := hmndata.FetchSnippet(c, c.Conn, c.CurrentUser, existingSnippetId, query)
		if err != nil {
			if errors.Is(err, db.NotFound) {
				return FourOhFour(c)
			} else {
				return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch existing snippet for edit"))
			}
		}
		originalText = snip.Snippet.Description
		embedUrl = snip.Snippet.Url
		assetID = snip.Snippet.AssetID
		if snip.Snippet.Url != nil {
			embedUrl = snip.Snippet.Url
		}
		existingSnippet = &snip
	}

	if strings.ToLower(action) == "delete" {
		if existingSnippet != nil {
			_, err = c.Conn.Exec(c,
				`
				DELETE FROM snippet
				WHERE snippet.id = $1
				`,
				existingSnippet.Snippet.ID,
			)
			if err != nil {
				return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch existing snippet for edit"))
			}
		} else {
			return FourOhFour(c)
		}
	} else {
		if form.Get("remove_attachment") == "true" {
			embedUrl = nil
			assetID = nil
		}
		text := strings.TrimSpace(form.Get("text"))
		textHtml := parsing.ParseMarkdown(text, parsing.DiscordMarkdown)
		projectAssociations := form["project_id"]
		var assetData *assets.CreateInput

		file, header, err := c.Req.FormFile("file")
		if err != nil && !errors.Is(err, http.ErrMissingFile) {
			return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get form file"))
		}

		if header != nil {
			content := make([]byte, header.Size)
			_, err = file.Read(content)
			if err != nil {
				return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to read uploaded file"))
			}
			contentType := header.Header.Get("Content-Type")
			if contentType == "" {
				contentType = http.DetectContentType(content)
			}
			width := 0
			height := 0
			if strings.HasPrefix(contentType, "image/") && contentType != "image/svg+xml" {
				file.Seek(0, io.SeekStart)
				config, _, err := image.DecodeConfig(file)
				if err == nil {
					width = config.Width
					height = config.Height
				}
			}
			assetData = &assets.CreateInput{
				Content:     content,
				Filename:    header.Filename,
				ContentType: contentType,
				UploaderID:  &c.CurrentUser.ID,
				Width:       width,
				Height:      height,
			}
		}

		if originalText != text && assetData == nil && embedUrl == nil && assetID == nil {
			urls := xurls.Relaxed().FindAllString(text, -1)
			if urls != nil {
				embeddable, err := embed.GetEmbeddableFromUrls(c, urls, maxUploadSize, time.Second*10, 3)
				if err != nil {
					if !errors.Is(err, embed.DownloadTooBigError) && !errors.Is(err, embed.NoEmbedFound) {
						c.Logger.Error().Err(err).Msg("failed to fetch embeddable for snippet")
					}
				} else {
					if embeddable.Url != "" {
						embedUrl = &embeddable.Url
					} else {
						width := 0
						height := 0
						if strings.HasPrefix(embeddable.File.ContentType, "image/") && embeddable.File.ContentType != "image/svg+xml" {
							reader := bytes.NewReader(embeddable.File.Data)
							config, _, err := image.DecodeConfig(reader)
							if err == nil {
								width = config.Width
								height = config.Height
							}
						}
						assetData = &assets.CreateInput{
							Content:     embeddable.File.Data,
							Filename:    embeddable.File.Filename,
							ContentType: embeddable.File.ContentType,
							UploaderID:  &c.CurrentUser.ID,
							Width:       width,
							Height:      height,
						}
					}
				}
			}
		}

		if text == "" && assetData == nil && embedUrl == nil && assetID == nil {
			return c.RejectRequest("You must provide a description or a file attachment.")
		}

		tx, err := c.Conn.Begin(c)
		if err != nil {
			return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to start transaction"))
		}
		defer tx.Rollback(c)

		var asset *models.Asset
		if assetData != nil {
			asset, err = assets.Create(c, tx, *assetData)
			if err != nil {
				return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create asset"))
			}
			assetID = &asset.ID
		}

		snippetId := 0
		if existingSnippet != nil {
			_, err = tx.Exec(c,
				`
				UPDATE snippet SET 
					url = $2,
					description = $3,
					_description_html = $4,
					asset_id = $5,
					edited_on_website = $6
				WHERE id = $1
				`,
				existingSnippet.Snippet.ID,
				embedUrl,
				text,
				textHtml,
				assetID,
				true,
			)
			if err != nil {
				return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update snippet"))
			}
			snippetId = existingSnippet.Snippet.ID
		} else {
			newSnippetId, err := db.QueryOne[int](c, tx,
				`
				INSERT INTO snippet (url, "when", description, _description_html, asset_id, owner_id, edited_on_website)
				VALUES ($1, $2, $3, $4, $5, $6 ,$7)
				RETURNING id
				`,
				embedUrl,
				time.Now(),
				text,
				textHtml,
				assetID,
				c.CurrentUser.ID,
				true,
			)
			if err != nil {
				return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to insert snippet"))
			}
			snippetId = *newSnippetId
		}

		_, err = tx.Exec(c,
			`
			DELETE FROM snippet_project
			WHERE snippet_id = $1
			`,
			snippetId,
		)

		if len(projectAssociations) > 0 {
			var projectIds []int
			for _, pidStr := range projectAssociations {
				projId, err := strconv.Atoi(pidStr)
				if err != nil {
					continue
				}
				projectIds = append(projectIds, projId)
			}

			if len(projectIds) > 0 {
				projectQuery := hmndata.ProjectsQuery{
					ProjectIDs: projectIds,
				}
				if !c.CurrentUser.IsStaff {
					projectQuery.OwnerIDs = []int{c.CurrentUser.ID}
				}
				projects, err := hmndata.FetchProjects(c, tx, c.CurrentUser, projectQuery)
				if err != nil {
					return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch projects for snippet"))
				}
				for _, p := range projects {
					_, err = tx.Exec(c,
						`
						INSERT INTO snippet_project (snippet_id, project_id, kind)
						VALUES ($1, $2, $3)
						`,
						snippetId,
						p.Project.ID,
						models.SnippetProjectKindWebsite,
					)
				}
			}
		}

		err = tx.Commit(c)
		if err != nil {
			return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit transaction"))
		}
	}

	return c.Redirect(redirect, http.StatusSeeOther)
}
