package website

import (
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"git.handmade.network/hmn/hmn/src/db"
	"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"
	"github.com/tcolgate/mp3"
)

type PodcastIndexData struct {
	templates.BaseData
	Podcast       templates.Podcast
	Episodes      []templates.PodcastEpisode
	EditUrl       string
	NewEpisodeUrl string
}

func PodcastIndex(c *RequestContext) ResponseData {
	podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, true, "")
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, err)
	}

	if podcastResult.Podcast == nil {
		return FourOhFour(c)
	}

	canEdit := c.CurrentUserCanEditCurrentProject

	baseData := getBaseDataAutocrumb(c, podcastResult.Podcast.Title)

	podcastIndexData := PodcastIndexData{
		BaseData: baseData,
		Podcast:  templates.PodcastToTemplate(podcastResult.Podcast, podcastResult.ImageFile),
	}

	if canEdit {
		podcastIndexData.EditUrl = hmnurl.BuildPodcastEdit()
		podcastIndexData.NewEpisodeUrl = hmnurl.BuildPodcastEpisodeNew()
	}

	for _, episode := range podcastResult.Episodes {
		podcastIndexData.Episodes = append(podcastIndexData.Episodes, templates.PodcastEpisodeToTemplate(episode, 0, podcastResult.ImageFile))
	}
	var res ResponseData
	err = res.WriteTemplate("podcast_index.html", podcastIndexData, c.Perf)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast index page"))
	}
	return res
}

type PodcastEditData struct {
	templates.BaseData
	Podcast templates.Podcast
}

func PodcastEdit(c *RequestContext) ResponseData {
	podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, false, "")
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, err)
	}

	canEdit := c.CurrentUserCanEditCurrentProject

	if podcastResult.Podcast == nil || !canEdit {
		return FourOhFour(c)
	}

	podcast := templates.PodcastToTemplate(podcastResult.Podcast, podcastResult.ImageFile)
	baseData := getBaseData(
		c,
		fmt.Sprintf("Edit %s", podcast.Title),
		[]templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}},
	)
	podcastEditData := PodcastEditData{
		BaseData: baseData,
		Podcast:  podcast,
	}

	var res ResponseData
	err = res.WriteTemplate("podcast_edit.html", podcastEditData, c.Perf)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast edit page"))
	}
	return res
}

func PodcastEditSubmit(c *RequestContext) ResponseData {
	podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, false, "")
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, err)
	}

	canEdit := c.CurrentUserCanEditCurrentProject

	if podcastResult.Podcast == nil || !canEdit {
		return FourOhFour(c)
	}

	c.Perf.StartBlock("PODCAST", "Handling file upload")
	c.Perf.StartBlock("PODCAST", "Parsing form")
	maxFileSize := int64(2 * 1024 * 1024)
	maxBodySize := maxFileSize + 1024*1024
	c.Req.Body = http.MaxBytesReader(c.Res, c.Req.Body, maxBodySize)
	err = c.Req.ParseMultipartForm(maxBodySize)
	c.Perf.EndBlock()
	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"))
	}

	title := c.Req.Form.Get("title")
	if len(strings.TrimSpace(title)) == 0 {
		return c.RejectRequest("Podcast title is empty")
	}
	description := c.Req.Form.Get("description")
	if len(strings.TrimSpace(description)) == 0 {
		return c.RejectRequest("Podcast description is empty")
	}

	c.Perf.StartBlock("SQL", "Updating podcast")
	tx, err := c.Conn.Begin(c)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to start db transaction"))
	}
	defer tx.Rollback(c)

	imageSaveResult := SaveImageFile(c, tx, "podcast_image", maxFileSize, fmt.Sprintf("podcast/%s/logo%d", c.CurrentProject.Slug, time.Now().UTC().Unix()))
	if imageSaveResult.ValidationError != "" {
		return c.RejectRequest(imageSaveResult.ValidationError)
	} else if imageSaveResult.FatalError != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(imageSaveResult.FatalError, "Failed to save podcast image"))
	}

	if imageSaveResult.ImageFile != nil {
		_, err = tx.Exec(c,
			`
			UPDATE podcast
			SET
				title = $1,
				description = $2,
				image_id = $3
			WHERE id = $4
			`,
			title,
			description,
			imageSaveResult.ImageFile.ID,
			podcastResult.Podcast.ID,
		)
		if err != nil {
			return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to update podcast"))
		}
	} else {
		_, err = tx.Exec(c,
			`
			UPDATE podcast
			SET
				title = $1,
				description = $2
			WHERE id = $3
			`,
			title,
			description,
			podcastResult.Podcast.ID,
		)
	}
	err = tx.Commit(c)
	c.Perf.EndBlock()
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to commit db transaction"))
	}

	res := c.Redirect(hmnurl.BuildPodcastEdit(), http.StatusSeeOther)
	res.AddFutureNotice("success", "Podcast updated successfully.")
	return res
}

type PodcastEpisodeData struct {
	templates.BaseData
	Podcast templates.Podcast
	Episode templates.PodcastEpisode
	EditUrl string
}

func PodcastEpisode(c *RequestContext) ResponseData {
	episodeGUIDStr := c.PathParams["episodeid"]

	podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, true, episodeGUIDStr)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, err)
	}

	if podcastResult.Podcast == nil || len(podcastResult.Episodes) == 0 {
		return FourOhFour(c)
	}

	canEdit := c.CurrentUserCanEditCurrentProject

	editUrl := ""
	if canEdit {
		editUrl = hmnurl.BuildPodcastEpisodeEdit(podcastResult.Episodes[0].GUID.String())
	}

	podcast := templates.PodcastToTemplate(podcastResult.Podcast, podcastResult.ImageFile)
	episode := templates.PodcastEpisodeToTemplate(podcastResult.Episodes[0], 0, podcastResult.ImageFile)
	baseData := getBaseData(
		c,
		fmt.Sprintf("%s | %s", episode.Title, podcast.Title),
		[]templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}},
	)

	podcastEpisodeData := PodcastEpisodeData{
		BaseData: baseData,
		Podcast:  podcast,
		Episode:  episode,
		EditUrl:  editUrl,
	}

	var res ResponseData
	err = res.WriteTemplate("podcast_episode.html", podcastEpisodeData, c.Perf)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast episode page"))
	}
	return res
}

type PodcastEpisodeEditData struct {
	templates.BaseData
	IsEdit        bool
	Title         string
	Description   string
	EpisodeNumber int
	CurrentFile   string
	EpisodeFiles  []string
}

func PodcastEpisodeNew(c *RequestContext) ResponseData {
	podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, false, "")
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, err)
	}

	canEdit := c.CurrentUserCanEditCurrentProject

	if podcastResult.Podcast == nil || !canEdit {
		return FourOhFour(c)
	}

	episodeFiles, err := GetEpisodeFiles(c.CurrentProject.Slug)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to fetch podcast episode file list"))
	}

	podcast := templates.PodcastToTemplate(podcastResult.Podcast, "")
	var res ResponseData
	baseData := getBaseData(
		c,
		fmt.Sprintf("New episode | %s", podcast.Title),
		[]templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}},
	)
	err = res.WriteTemplate("podcast_episode_edit.html", PodcastEpisodeEditData{
		BaseData:     baseData,
		IsEdit:       false,
		EpisodeFiles: episodeFiles,
	}, c.Perf)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast episode new page"))
	}
	return res
}

func PodcastEpisodeEdit(c *RequestContext) ResponseData {
	episodeGUIDStr, found := c.PathParams["episodeid"]
	if !found || episodeGUIDStr == "" {
		return FourOhFour(c)
	}

	podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, true, episodeGUIDStr)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, err)
	}

	canEdit := c.CurrentUserCanEditCurrentProject

	if podcastResult.Podcast == nil || len(podcastResult.Episodes) == 0 || !canEdit {
		return FourOhFour(c)
	}

	episodeFiles, err := GetEpisodeFiles(c.CurrentProject.Slug)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to fetch podcast episode file list"))
	}
	episode := podcastResult.Episodes[0]

	podcast := templates.PodcastToTemplate(podcastResult.Podcast, "")
	podcastEpisode := templates.PodcastEpisodeToTemplate(episode, 0, "")
	baseData := getBaseData(
		c,
		fmt.Sprintf("Edit episode %s | %s", podcastEpisode.Title, podcast.Title),
		[]templates.Breadcrumb{{Name: podcast.Title, Url: podcast.Url}, {Name: podcastEpisode.Title, Url: podcastEpisode.Url}},
	)
	podcastEpisodeEditData := PodcastEpisodeEditData{
		BaseData:      baseData,
		IsEdit:        true,
		Title:         episode.Title,
		Description:   episode.Description,
		EpisodeNumber: episode.EpisodeNumber,
		CurrentFile:   episode.AudioFile,
		EpisodeFiles:  episodeFiles,
	}

	var res ResponseData
	err = res.WriteTemplate("podcast_episode_edit.html", podcastEpisodeEditData, c.Perf)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast episode edit page"))
	}
	return res
}

func PodcastEpisodeSubmit(c *RequestContext) ResponseData {
	episodeGUIDStr, found := c.PathParams["episodeid"]

	isEdit := found && episodeGUIDStr != ""
	podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, isEdit, episodeGUIDStr)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, err)
	}

	canEdit := c.CurrentUserCanEditCurrentProject

	if podcastResult.Podcast == nil || (isEdit && len(podcastResult.Episodes) == 0) || !canEdit {
		return FourOhFour(c)
	}

	c.Perf.StartBlock("OS", "Fetching podcast episode files")
	episodeFiles, err := GetEpisodeFiles(c.CurrentProject.Slug)
	c.Perf.EndBlock()
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to fetch podcast episode file list"))
	}

	c.Req.ParseForm()
	title := c.Req.Form.Get("title")
	if len(strings.TrimSpace(title)) == 0 {
		return c.RejectRequest("Episode title is empty")
	}
	description := c.Req.Form.Get("description")
	if len(strings.TrimSpace(description)) == 0 {
		return c.RejectRequest("Episode description is empty")
	}
	episodeNumberStr := c.Req.Form.Get("episode_number")
	episodeNumber, err := strconv.Atoi(episodeNumberStr)
	if err != nil {
		return c.RejectRequest("Episode number can't be parsed")
	}
	episodeFile := c.Req.Form.Get("episode_file")
	found = false
	for _, ef := range episodeFiles {
		if episodeFile == ef {
			found = true
			break
		}
	}

	if !found {
		return c.RejectRequest("Requested episode file not found")
	}

	c.Perf.StartBlock("MP3", "Parsing mp3 file for duration")
	file, err := os.Open(fmt.Sprintf("public/media/podcast/%s/%s", c.CurrentProject.Slug, episodeFile))
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to open podcast file"))
	}

	mp3Decoder := mp3.NewDecoder(file)
	var duration float64
	skipped := 0
	var decodingError error
	var f mp3.Frame
	for {
		if err = mp3Decoder.Decode(&f, &skipped); err != nil {
			if err == io.EOF {
				break
			} else {
				decodingError = err
				break
			}
		}
		duration = duration + f.Duration().Seconds()
	}
	file.Close()
	c.Perf.EndBlock()
	if decodingError != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to decode mp3 file"))
	}

	c.Perf.StartBlock("MARKDOWN", "Parsing description")
	descriptionRendered := parsing.ParseMarkdown(description, parsing.ForumRealMarkdown)
	c.Perf.EndBlock()

	guidStr := ""
	if isEdit {
		guidStr = podcastResult.Episodes[0].GUID.String()
		c.Perf.StartBlock("SQL", "Updating podcast episode")
		_, err := c.Conn.Exec(c,
			`
			UPDATE podcast_episode
			SET
				title = $1,
				description = $2,
				description_rendered = $3,
				audio_filename = $4,
				duration = $5,
				episode_number = $6
			WHERE
				guid = $7
			`,
			title,
			description,
			descriptionRendered,
			episodeFile,
			duration,
			episodeNumber,
			podcastResult.Episodes[0].GUID,
		)
		c.Perf.EndBlock()
		if err != nil {
			return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to update podcast episode"))
		}
	} else {
		guid := uuid.New()
		guidStr = guid.String()
		c.Perf.StartBlock("SQL", "Creating new podcast episode")
		_, err := c.Conn.Exec(c,
			`
			INSERT INTO podcast_episode
				(guid, title, description, description_rendered, audio_filename, duration, pub_date, episode_number, podcast_id)
			VALUES
				($1, $2, $3, $4, $5, $6, $7, $8, $9)
			`,
			guid,
			title,
			description,
			descriptionRendered,
			episodeFile,
			duration,
			time.Now(),
			episodeNumber,
			podcastResult.Podcast.ID,
		)
		c.Perf.EndBlock()
		if err != nil {
			return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to create podcast episode"))
		}
	}

	res := c.Redirect(hmnurl.BuildPodcastEpisodeEdit(guidStr), http.StatusSeeOther)
	res.AddFutureNotice("success", "Podcast episode updated successfully.")
	return res
}

func GetEpisodeFiles(projectSlug string) ([]string, error) {
	folderStr := fmt.Sprintf("public/media/podcast/%s/", projectSlug)
	folder := os.DirFS(folderStr)
	files, err := fs.Glob(folder, "*.mp3")
	return files, err
}

type PodcastRSSData struct {
	Podcast  templates.Podcast
	Episodes []templates.PodcastEpisode
}

func PodcastRSS(c *RequestContext) ResponseData {
	podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, true, "")
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, err)
	}

	if podcastResult.Podcast == nil {
		return FourOhFour(c)
	}

	podcastRSSData := PodcastRSSData{
		Podcast: templates.PodcastToTemplate(podcastResult.Podcast, podcastResult.ImageFile),
	}

	for _, episode := range podcastResult.Episodes {
		var filesize int64
		stat, err := os.Stat(fmt.Sprintf("./public/media/podcast/%s/%s", c.CurrentProject.Slug, episode.AudioFile))
		if err != nil {
			c.Logger.Err(err).Msg("Couldn't get filesize for podcast episode")
		} else {
			filesize = stat.Size()
		}
		podcastRSSData.Episodes = append(podcastRSSData.Episodes, templates.PodcastEpisodeToTemplate(episode, filesize, podcastResult.ImageFile))
	}

	var res ResponseData
	err = res.WriteTemplate("podcast.xml", podcastRSSData, c.Perf)
	if err != nil {
		return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast RSS"))
	}
	return res
}

type PodcastResult struct {
	Podcast   *models.Podcast
	ImageFile string
	Episodes  []*models.PodcastEpisode
}

func FetchPodcast(c *RequestContext, projectId int, fetchEpisodes bool, episodeGUID string) (PodcastResult, error) {
	var result PodcastResult
	c.Perf.StartBlock("SQL", "Fetch podcast")
	type podcastQuery struct {
		Podcast       models.Podcast `db:"podcast"`
		ImageFilename string         `db:"imagefile.file"`
	}
	podcastQueryResult, err := db.QueryOne[podcastQuery](c, c.Conn,
		`
		SELECT $columns
		FROM
			podcast
			LEFT JOIN image_file AS imagefile ON imagefile.id = podcast.image_id
		WHERE podcast.project_id = $1
		`,
		projectId,
	)
	c.Perf.EndBlock()
	if err != nil {
		if errors.Is(err, db.NotFound) {
			return result, nil
		} else {
			return result, oops.New(err, "failed to fetch podcast")
		}
	}
	podcast := podcastQueryResult.Podcast
	podcastImageFilename := podcastQueryResult.ImageFilename
	result.Podcast = &podcast
	result.ImageFile = podcastImageFilename

	if fetchEpisodes {
		if episodeGUID == "" {
			c.Perf.StartBlock("SQL", "Fetch podcast episodes")
			episodes, err := db.Query[models.PodcastEpisode](c, c.Conn,
				`
				SELECT $columns
				FROM podcast_episode AS episode
				WHERE episode.podcast_id = $1
				ORDER BY episode.season_number DESC, episode.episode_number DESC
				`,
				podcast.ID,
			)
			c.Perf.EndBlock()
			if err != nil {
				return result, oops.New(err, "failed to fetch podcast episodes")
			}
			result.Episodes = episodes
		} else {
			guid, err := uuid.Parse(episodeGUID)
			if err != nil {
				return result, err
			}
			c.Perf.StartBlock("SQL", "Fetch podcast episode")
			episode, err := db.QueryOne[models.PodcastEpisode](c, c.Conn,
				`
				SELECT $columns
				FROM podcast_episode AS episode
				WHERE episode.podcast_id = $1 AND episode.guid = $2
				`,
				podcast.ID,
				guid,
			)
			c.Perf.EndBlock()
			if err != nil {
				if errors.Is(err, db.NotFound) {
					return result, nil
				} else {
					return result, oops.New(err, "failed to fetch podcast episode")
				}
			}
			result.Episodes = append(result.Episodes, episode)
		}
	}

	return result, nil
}
