package main

import (
	"errors"
	"fmt"
	"html/template"
	"image"
	"io"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	_ "image/jpeg"
	_ "image/png"

	"coffey.dad/internal/models"
	"coffey.dad/internal/validator"

	"github.com/julienschmidt/httprouter"
)

func (app *application) home(w http.ResponseWriter, r *http.Request) {
	posts, err := app.posts.Latest(5)
	if err != nil {
		app.serverError(w, r, err)
		return
	}

	data := app.newTemplateData(r)
	data.Posts = posts

	app.render(w, r, http.StatusOK, "home.tmpl", data)
}

func (app *application) postView(w http.ResponseWriter, r *http.Request) {
	var post models.Post
	params := httprouter.ParamsFromContext(r.Context())
	idparam := params.ByName("id")

	id, err := strconv.Atoi(idparam)
	if err != nil {
		post, err = app.posts.GetByURL(idparam)
		if err != nil {
			if errors.Is(err, models.ErrNoRecord) {
				app.notFound(w, r)
			} else {
				app.serverError(w, r, err)
			}
			return
		}
	} else {
		post, err = app.posts.Get(id)
		if err != nil {
			if errors.Is(err, models.ErrNoRecord) {
				app.notFound(w, r)
			} else {
				app.serverError(w, r, err)
			}
			return
		}
	}

	if post.IsDraft {
		app.notFound(w, r)
		return
	}

	post.ParseBody()

	flash := app.sessionManager.PopString(r.Context(), "flash")
	data := app.newTemplateData(r)
	data.Post = post
	data.Flash = flash

	app.logger.Info("Post Info", "Header Path", post.HeaderImage.Location, "Header MIME", post.HeaderImage.Mime)

	app.render(w, r, http.StatusOK, "post_view.tmpl", data)
}

func (app *application) postList(w http.ResponseWriter, r *http.Request) {
	filter, err := url.PathUnescape(r.URL.Query().Get("filter"))
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

	var posts []models.Post
	if filter == "" {
		posts, err = app.posts.All()
	} else {
		posts, err = app.posts.Filter(false, filter)
	}
	if err != nil {
		app.serverError(w, r, err)
		return
	}

	tags, err := app.posts.AllUsedTags()
	if err != nil {
		app.serverError(w, r, err)
		return
	}

	flash := app.sessionManager.PopString(r.Context(), "flash")
	data := app.newTemplateData(r)
	data.Posts = posts
	data.Tags = tags
	data.Flash = flash
	data.Filter = filter

	app.render(w, r, http.StatusOK, "post_list.tmpl", data)
}

func (app *application) draftList(w http.ResponseWriter, r *http.Request) {
	drafts, err := app.posts.AllDrafts()
	if err != nil {
		app.serverError(w, r, err)
		return
	}

	flash := app.sessionManager.PopString(r.Context(), "flash")
	data := app.newTemplateData(r)
	data.Posts = drafts
	data.Flash = flash

	app.render(w, r, http.StatusOK, "draft_list.tmpl", data)
}

func (app *application) repoView(w http.ResponseWriter, r *http.Request) {
	data := app.newTemplateData(r)

	params := httprouter.ParamsFromContext(r.Context())
	name := params.ByName("name")
	objectType := params.ByName("type")
	branch := params.ByName("branch")
	path := strings.Trim(params.ByName("path"), "/")

	// app.logger.Debug("params", "name", name, "objectType", objectType, "branch", branch, "path", path)

	repo, err := app.repos.GetByName(name)
	if err != nil {
		app.logger.Info("GetByName error")
		app.notFound(w, r)
		return
	}

	data.Repo = repo
	data.Branch = branch
	data.Path = path
	data.ParentPath = strings.Trim(filepath.Dir("/" + path), "/")
	// Reusing FileNames for the path parts, surely will not cause future me any confusion...
	if data.ParentPath != "" {
		data.FileNames = strings.Split(data.ParentPath, "/")
	}
	data.CurrentObject = filepath.Base(path)
	// app.logger.Info("Values", "Path", data.Path, "ParentPath", data.ParentPath)
	flash := app.sessionManager.PopString(r.Context(), "flash")
	data.Flash = flash

	if app.repos.CountRevs(repo) == 0 {
		app.render(w, r, http.StatusOK, "repo_view_empty.tmpl", data)
		return
	}

	if objectType == "tree" || (objectType == "" && branch == "" && path == "") {
		if branch == "" {
			var err error
			branch, err = app.repos.GetHeadBranch(name)
			if err != nil {
				app.serverError(w, r, err)
				return
			}
			data.Branch = branch
		}

		objects, err := app.repos.GetTreeObjects(name, branch, path)
		if err != nil {
			if errors.Is(err, models.ErrNoRepo) {
				// app.logger.Info("GetTreeObjects error")
				app.notFound(w, r)
			} else {
				exitErr, ok := err.(*exec.ExitError)
				if ok {
					app.logger.Error(string(exitErr.Stderr))
				}
				app.serverError(w, r, err)
			}
			return
		}

		data.GitObjects = objects
		app.render(w, r, http.StatusOK, "repo_view_tree.tmpl", data)
		return
	}

	if objectType == "blob"{
		if branch == "" {
			var err error
			branch, err = app.repos.GetHeadBranch(name)
			if err != nil {
				app.serverError(w, r, err)
				return
			}
			data.Branch = branch
		}
		text, err := app.repos.GetBlobObjectText(name, branch, path)
		if err != nil {
			if errors.Is(err, models.ErrNoRepo) {
				app.notFound(w, r)
			}
			app.serverError(w, r, err)
			return
		}

		data.Text = text
		app.render(w, r, http.StatusOK, "repo_view_blob.tmpl", data)
		return
	}
	// app.logger.Info("default case hit, objectType issue")
	app.notFound(w, r)
}

func (app *application) repoList(w http.ResponseWriter, r *http.Request) {
	data := app.newTemplateData(r)
	var err error
	data.Repos, err = app.repos.GetAll()
	if err != nil {
		app.serverError(w, r, err)
		return
	}
	flash := app.sessionManager.PopString(r.Context(), "flash")
	data.Flash = flash
	app.render(w, r, http.StatusOK, "repo_list.tmpl", data)
}

type repoForm struct {
	Name                string `form:"name"`
	Description         string `form:"desc"`
	validator.Validator `form:"-"`
}

func (app *application) addRepo(w http.ResponseWriter, r *http.Request) {
	form := repoForm{}
	data := app.newTemplateData(r)
	data.Form = form

	app.render(w, r, http.StatusOK, "repo_add.tmpl", data)
}

func (app *application) addRepoPost(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

	var form repoForm

	err = app.formDecoder.Decode(&form, r.PostForm)
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

	form.CheckField(validator.NotBlank(form.Name), "name", "Name cannot be blank")
	form.CheckField(validator.MaxChars(form.Name, 64), "name", "Name cannot be longer than 64 characters")

	validChars := regexp.MustCompile(`^[a-zA-Z0-9\-_\.]+$`).MatchString
	form.CheckField(validChars(form.Name), "name", "Name can only contain ASCII letters, digits, -, _, and .")

	if !form.Valid() {
		data := app.newTemplateData(r)
		data.Form = form
		app.render(w, r, http.StatusUnprocessableEntity, "repo_add.tmpl", data)
		return
	}

	err = app.repos.CreateRepo(form.Name, form.Description)
	if err != nil {
		app.serverError(w, r, err)
		return
	}

	app.sessionManager.Put(r.Context(), "flash", "Repo created")
	http.Redirect(w, r, fmt.Sprintf("/repo/%s", form.Name), http.StatusSeeOther)
}

type postForm struct {
	Title               string `form:"title"`
	URL                 string `form:"url"`
	Tags                []models.Tag
	Body                string `form:"body"`
	IsDraft             bool   `form:"asDraft"`
	HeaderImageLocation string
	Description         string
	validator.Validator `form:"-"`
}

func (app *application) newPost(w http.ResponseWriter, r *http.Request) {
	form := postForm{}
	data := app.newTemplateData(r)
	data.NewPost = true
	data.Form = form

	tags, err := app.posts.AllTags()
	if err != nil {
		app.serverError(w, r, err)
		return
	}
	data.Tags = tags

	app.render(w, r, http.StatusOK, "add_edit_post.tmpl", data)
}

func (app *application) newPostSubmit(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

	var form postForm

	err = app.formDecoder.Decode(&form, r.PostForm)
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

	form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
	form.CheckField(validator.MaxChars(form.Title, 256), "title", "This field cannot be longer than 256 characters")
	form.CheckField(validator.NotBlank(form.Body), "body", "This field cannot be blank")
	form.CheckField(validator.NotBlank(form.URL), "url", "This field cannot be blank")
	form.CheckField(validator.MaxChars(form.URL, 200), "url", "This field cannot be longer than 200 characters")

	if !form.Valid() {
		data := app.newTemplateData(r)
		data.Form = form
		data.NewPost = true
		app.render(w, r, http.StatusUnprocessableEntity, "add_edit_post.tmpl", data)
		return
	}

	form.URL = url.PathEscape(form.URL)

	now := time.Now()
	_, err = app.posts.Insert(models.Post{
		Title:    form.Title,
		Body:     template.HTML(form.Body),
		Tags:     form.Tags,
		IsDraft:  form.IsDraft,
		Created:  now,
		Modified: now,
		URL:      form.URL,
		HeaderImage: models.Image{
			Location: form.HeaderImageLocation,
		},
		Description: form.Description,
	})

	if err != nil {
		if errors.Is(err, models.ErrDuplicateUrl) {
			form.AddFieldError("url", "URL already exists")

			data := app.newTemplateData(r)
			data.Form = form
			app.render(w, r, http.StatusUnprocessableEntity, "add_edit_post.tmpl", data)
		} else {
			app.serverError(w, r, err)
		}
		return
	}

	if form.IsDraft {
		app.sessionManager.Put(r.Context(), "flash", "Saved")
		http.Redirect(w, r, "/blog/drafts", http.StatusSeeOther)
	} else {
		app.sessionManager.Put(r.Context(), "flash", "Published successfully!")
		http.Redirect(w, r, fmt.Sprintf("/blog/post/%s", form.URL), http.StatusSeeOther)
	}
}

func (app *application) editPost(w http.ResponseWriter, r *http.Request) {
	params := httprouter.ParamsFromContext(r.Context())

	id, err := strconv.Atoi(params.ByName("id"))
	if err != nil {
		app.notFound(w, r)
		return
	}

	post, err := app.posts.Get(id)
	if err != nil {
		if errors.Is(err, models.ErrNoRecord) {
			app.notFound(w, r)
		} else {
			app.serverError(w, r, err)
		}
		return
	}

	form := postForm{
		Title:               post.Title,
		URL:                 post.URL,
		Tags:                post.Tags,
		Body:                string(post.Body),
		HeaderImageLocation: post.HeaderImage.Location,
		Description:         post.Description,
	}

	data := app.newTemplateData(r)
	data.Post = post
	data.Form = form
	data.NewPost = false

	tags, err := app.posts.AllTags()
	if err != nil {
		app.serverError(w, r, err)
		return
	}
	data.Tags = tags

	app.render(w, r, http.StatusOK, "add_edit_post.tmpl", data)
}

func (app *application) editPostSubmit(w http.ResponseWriter, r *http.Request) {
	params := httprouter.ParamsFromContext(r.Context())

	id, err := strconv.Atoi(params.ByName("id"))
	if err != nil {
		app.notFound(w, r)
		return
	}

	err = r.ParseForm()
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

	var form postForm

	err = app.formDecoder.Decode(&form, r.PostForm)
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

	form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
	form.CheckField(validator.MaxChars(form.Title, 256), "title", "This field cannot be longer than 256 characters")
	form.CheckField(validator.NotBlank(form.Body), "body", "This field cannot be blank")
	form.CheckField(validator.NotBlank(form.URL), "url", "This field cannot be blank")
	form.CheckField(validator.MaxChars(form.URL, 200), "url", "This field cannot be longer than 200 characters")

	if !form.Valid() {
		data := app.newTemplateData(r)
		data.Form = form
		data.NewPost = false
		app.render(w, r, http.StatusUnprocessableEntity, "add_edit_post.tmpl", data)
		return
	}

	p, err := app.posts.Get(id)
	if err != nil {
		app.serverError(w, r, err)
		return
	}

	now := time.Now()
	// If we are publishing a draft
	if !form.IsDraft && p.IsDraft {
		// I should probably create a new column called "published", but this
		// is fine for now
		p.Created = now
	}

	p.Title = form.Title
	p.Body = template.HTML(form.Body)
	p.IsDraft = form.IsDraft
	p.URL = url.PathEscape(form.URL)
	p.Tags = form.Tags
	p.Modified = now
	p.HeaderImage = models.Image{
		Location: form.HeaderImageLocation,
	}
	p.Description = form.Description

	err = app.posts.Update(p)
	if err != nil {
		if errors.Is(err, models.ErrDuplicateUrl) {
			form.AddFieldError("url", "URL already exists")

			data := app.newTemplateData(r)
			data.Form = form
			app.render(w, r, http.StatusUnprocessableEntity, "add_edit_post.tmpl", data)
		} else {
			app.serverError(w, r, err)
		}
		return
	}

	if form.IsDraft {
		app.sessionManager.Put(r.Context(), "flash", "Saved")
		http.Redirect(w, r, "/blog/drafts", http.StatusSeeOther)
	} else {
		app.sessionManager.Put(r.Context(), "flash", "Published successfully!")
		http.Redirect(w, r, fmt.Sprintf("/blog/post/%s", p.URL), http.StatusSeeOther)
	}
}

type loginForm struct {
	Username string
	Password string
	validator.Validator
}

func (app *application) login(w http.ResponseWriter, r *http.Request) {
	data := app.newTemplateData(r)
	data.Form = loginForm{}
	app.render(w, r, http.StatusOK, "login.tmpl", data)
}

func (app *application) loginSubmit(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

	form := loginForm{
		Username: r.PostForm.Get("username"),
		Password: r.PostForm.Get("password"),
	}

	form.CheckField(validator.NotBlank(form.Username), "username", "This field cannot be blank")
	form.CheckField(validator.NotBlank(form.Password), "password", "Thid field cannot be blank")

	if !form.Valid() {
		data := app.newTemplateData(r)
		data.Form = form
		app.render(w, r, http.StatusUnprocessableEntity, "login.tmpl", data)
		return
	}

	id, err := app.users.Authenticate(form.Username, form.Password)
	if err != nil {
		if errors.Is(err, models.ErrInvalidCredentials) {
			form.AddNonFieldError("Username or password is incorrect")

			data := app.newTemplateData(r)
			data.Form = form
			app.render(w, r, http.StatusUnprocessableEntity, "login.tmpl", data)
		} else {
			app.serverError(w, r, err)
		}
		return
	}

	err = app.sessionManager.RenewToken(r.Context())
	if err != nil {
		app.serverError(w, r, err)
		return
	}

	app.sessionManager.Put(r.Context(), "authenticatedUserID", id)
	redirect := app.sessionManager.PopString(r.Context(), "redirectPathAfterLogin")
	if redirect == "" {
		redirect = "/"
	}

	http.Redirect(w, r, redirect, http.StatusSeeOther)
}

func (app *application) logoutSubmit(w http.ResponseWriter, r *http.Request) {
	err := app.sessionManager.RenewToken(r.Context())
	if err != nil {
		app.serverError(w, r, err)
	}

	app.sessionManager.Remove(r.Context(), "authenticatedUserID")

	http.Redirect(w, r, "/", http.StatusSeeOther)
}

type uploadForm struct {
	validator.Validator
}

func (app *application) upload(w http.ResponseWriter, r *http.Request) {
	data := app.newTemplateData(r)
	data.Form = uploadForm{}

	app.render(w, r, http.StatusOK, "upload.tmpl", data)
}

func (app *application) uploadPost(w http.ResponseWriter, r *http.Request) {
	form := uploadForm{}

	err := r.ParseMultipartForm(32 << 20)
	if err != nil {
		form.AddNonFieldError("Error Uploading Image")

		data := app.newTemplateData(r)
		data.Form = form
		app.render(w, r, http.StatusInternalServerError, "upload.tmpl", data)
		return
	}

	files := r.MultipartForm.File["file"]

	for _, fileHeader := range files {
		file, err := fileHeader.Open()
		if err != nil {
			form.AddNonFieldError("Error Uploading Image")

			data := app.newTemplateData(r)
			data.Form = form
			app.render(w, r, http.StatusInternalServerError, "upload.tmpl", data)
			return
		}

		defer file.Close()

		buff := make([]byte, 512)
		_, err = file.Read(buff)
		if err != nil {
			form.AddNonFieldError("Error Uploading Image")

			data := app.newTemplateData(r)
			data.Form = form
			app.render(w, r, http.StatusInternalServerError, "upload.tmpl", data)
			return
		}

		_, err = file.Seek(0, io.SeekStart)
		if err != nil {
			form.AddNonFieldError("Error Uploading Image")

			data := app.newTemplateData(r)
			data.Form = form
			app.render(w, r, http.StatusInternalServerError, "upload.tmpl", data)
			return
		}

		err = os.MkdirAll("./uploads", os.ModePerm)
		if err != nil {
			form.AddNonFieldError("Error Uploading Image")

			data := app.newTemplateData(r)
			data.Form = form
			app.render(w, r, http.StatusInternalServerError, "upload.tmpl", data)
			return
		}

		f, err := os.Create(fmt.Sprintf("./uploads/%s", fileHeader.Filename))
		if err != nil {
			form.AddNonFieldError("Error Uploading Image")

			data := app.newTemplateData(r)
			data.Form = form
			app.render(w, r, http.StatusInternalServerError, "upload.tmpl", data)
			return
		}

		defer f.Close()

		_, err = io.Copy(f, file)
		if err != nil {
			form.AddNonFieldError("Error Uploading Image")

			data := app.newTemplateData(r)
			data.Form = form
			app.render(w, r, http.StatusInternalServerError, "upload.tmpl", data)
			return
		}
	}

	data := app.newTemplateData(r)
	data.Form = form
	data.Flash = "Image uploaded successfully!"
	app.render(w, r, http.StatusOK, "upload.tmpl", data)
}

func (app *application) imagePicker(w http.ResponseWriter, r *http.Request) {
	files, err := os.Open("./uploads/")
	if err != nil {
		app.serverError(w, r, err)
		return
	}
	defer files.Close()

	fileNames, err := files.Readdirnames(0)

	data := app.newTemplateData(r)
	data.FileNames = fileNames

	app.render(w, r, http.StatusOK, "imagepicker.tmpl", data)
}

func (app *application) headerImagePicker(w http.ResponseWriter, r *http.Request) {
	uploads, err := os.Open("./uploads/")
	if err != nil {
		app.serverError(w, r, err)
		return
	}
	defer uploads.Close()

	fileNames, err := uploads.Readdirnames(0)
	if err != nil {
		app.serverError(w, r, err)
		return
	}

	var finalFileNames []string
	for _, n := range fileNames {
		path := "./uploads/" + n
		bytes, err := os.ReadFile(path)
		if err != nil {
			app.serverError(w, r, err)
			return
		}

		mime := http.DetectContentType(bytes)
		if mime != "image/jpeg" && mime != "image/png" {
			continue
		}

		f, err := os.Open(path)
		if err != nil {
			app.serverError(w, r, err)
			return
		}
		defer f.Close()

		i, _, err := image.DecodeConfig(f)
		if err != nil {
			app.serverError(w, r, err)
			return
		}

		if i.Width != 1200 || i.Height != 630 {
			continue
		}

		finalFileNames = append(finalFileNames, n)
	}

	data := app.newTemplateData(r)
	app.logger.Info(data.CurrentPage)
	data.FileNames = finalFileNames
	app.render(w, r, http.StatusOK, "imagepicker.tmpl", data)
}