package main

import (
	"bufio"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"text/template"
)

type Post struct {
	Time      int
	Meta      []byte
	Thumbnail []byte
	Content   []byte
}

type Posts struct {
	Contents []*Post
}

func loadPosts(location string, postCount int) (p *Posts, err error) {
	if postCount == 0 || postCount < -1 {
		return nil, os.ErrInvalid
	}

	var tmpPosts []*Post

	if postCount == 1 {
		tmpPosts = append(tmpPosts, readFile(location+".html"))
	} else {
		var depthCount = 0

		// TODO - Use less computationally heavy opperation for scalability
		err = filepath.Walk(location, func(path string, _ os.FileInfo, _ error) error {
			var directory, err = regexp.MatchString("^data/(projects|blog)$", path)
			if err != nil {
				return err
			}

			if directory {
				depthCount++
			} else if postCount == -1 || depthCount < postCount+1 {
				tmp := readFile(path)
				tmpPosts = append(tmpPosts, tmp)
				depthCount++

			} else {
				depthCount++
			}

			return nil
		})

		if err != nil {
			return nil, err
		}

		sort.Slice(tmpPosts, func(i, j int) bool {
			return tmpPosts[i].Time > tmpPosts[j].Time
		})

	}

	return &Posts{Contents: tmpPosts}, nil
}

func readFile(location string) (p *Post) {
	file, err := os.Open(location)
	if err != nil {
		log.Fatal(err)
	}

	scanner := bufio.NewScanner(file)

	var section = 0
	var tmp *Post = new(Post)

LineLoop:
	for scanner.Scan() {
		var line = scanner.Text()
		var lineByte = []byte(line + "\n")

		if line == "{{br}}" { // this is not happy with any space or tab characters that follow,
			section++     // so make sure to check if there's whitespace after a {{br}} if
			goto LineLoop // there are any errors. I'll fix this later...TM
		}

		switch section {
		case 0:
			time, err := strconv.Atoi(strings.TrimSuffix(string(lineByte), "\n"))
			if err != nil {
				log.Fatal(err)
			} else {
				tmp.Time = time
			}
		case 1:
			tmp.Meta = append(tmp.Meta, lineByte...)
		case 2:
			tmp.Thumbnail = append(tmp.Thumbnail, lineByte...)
		case 3:
			tmp.Content = append(tmp.Content, lineByte...)
		}
	}

	if err := scanner.Err(); err != nil {
		log.Fatal(err)
	}

	file.Close()
	return tmp
}

func renderTemplate(w http.ResponseWriter, tmpl string, p *Posts) {
	if tmpl == "post" {
		t, err := template.ParseFiles("templates/master.html", "templates/postHelp.tmpl")
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		err = t.Execute(w, p.Contents[0])
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	} else {
		t, err := template.ParseFiles("templates/master.html", "templates/"+tmpl+".html")
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		err = t.Execute(w, p)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}

func rootHandler(w http.ResponseWriter, r *http.Request) {
	path := strings.Split(r.URL.Path, "/")
	if path[1] != "" {
		http.NotFound(w, r)
	} else {
		p, err := loadPosts("data/projects", 3)
		if err != nil {
			http.NotFound(w, r)
		}

		renderTemplate(w, "index", p)
	}
}

func aboutHandler(w http.ResponseWriter, r *http.Request) {
	renderTemplate(w, "about", nil)
}

func agHandler(w http.ResponseWriter, r *http.Request) {
	p, err := loadPosts("data"+strings.TrimSuffix(r.URL.Path, "/"), -1)
	if err != nil {
		http.NotFound(w, r)
	}

	renderTemplate(w, strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/"), p)
}

func postHandler(w http.ResponseWriter, r *http.Request) {
	path := strings.Split(r.URL.Path, "/")
	if path[2] == "" {
		agHandler(w, r)
	} else {
		p, err := loadPosts("data/"+path[1]+"/"+path[2], 1)
		if p == nil || err != nil {
			http.NotFound(w, r)
		} else {
			renderTemplate(w, "post", p)
		}
	}
}

func fileHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := os.ReadFile("data/www/" + r.URL.Path)

	w.Header().Set("Content-Type", "text/css; charset=utf-8")
	fmt.Fprintf(w, "%s", body)
}

func makeHandler(fn func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		fn(w, r)
	}
}

func httpsRedirect(w http.ResponseWriter, req *http.Request) {
	// remove/add not default ports from req.Host
	target := "https://" + req.Host + req.URL.Path
	if len(req.URL.RawQuery) > 0 {
		target += "?" + req.URL.RawQuery
	}
	log.Printf("redirect to: %s", target)
	http.Redirect(w, req, target,
		// see comments below and consider the codes 308, 302, or 301
		http.StatusMovedPermanently)
}

func main() {
	/* 	hostValid, _ := regexp.MatchString("^alexscerba.com$", r.URL.Path)
	   	if !hostValid {
	   		req, _ := http.NewRequest(r.Method, "alexscerba.com"+r.URL.Path, r.Body)
	   		redirect(w, req)
	   	} */

	fs := http.FileServer(http.Dir("./data/static"))
	http.Handle("/static/", http.StripPrefix("/static/", fs))

	http.HandleFunc("/projects/", postHandler)
	http.HandleFunc("/blog/", postHandler)
	http.HandleFunc("/about/", aboutHandler)
	http.HandleFunc("/", rootHandler)

	go http.ListenAndServe(":80", http.HandlerFunc(httpsRedirect))
	log.Fatal(http.ListenAndServeTLS(":443", "/etc/letsencrypt/live/alexscerba.com/fullchain.pem", "/etc/letsencrypt/live/alexscerba.com/privkey.pem", nil))
	//log.Fatal(http.ListenAndServe(":4000", nil)) // for local dev because I'm lazy
}