package main

import (
	"context"
	"flag"
	"fmt"
	"git.sr.ht/~adnano/go-gemini"
	"google.golang.org/grpc/credentials/insecure"
	"io"
	"log"
	"mime"
	"path"
	"regexp"
	"strings"
	"time"

	"git.sr.ht/~adnano/go-gemini/certificate"
	"github.com/flosch/pongo2/v6"
	"github.com/gorilla/feeds"
	"google.golang.org/grpc"
)

var (
	hostname        string
	port            string
	fileSrvHost     string
	fileSrvPort     string
	certificatePath string
	key             = "WgiPGpt5wM6SVEWo5iqI"
)

const DefaultLang = "sgs"

var SupportedLang = []string{"en", "sgs", "lt", "prg", "eo"}

func init() {
	flag.StringVar(&hostname, "hostname", "", "capsule hostname")
	flag.StringVar(&port, "port", "1965", "capsule port")
	flag.StringVar(&certificatePath, "certificatePath", "", "capsule certificate path")
	flag.StringVar(&fileSrvHost, "fileSrvHost", "", "file push server host")
	flag.StringVar(&fileSrvPort, "fileSrvPort", "", "file push server port")
	flag.StringVar(&key, "key", "", "secret key to communicate admin")
	flag.Parse()
}

func main() {
	certificates := &certificate.Store{}
	certificates.Register(hostname)
	if err := certificates.Load(certificatePath); err != nil {
		panic(err)
	}

	mux := &gemini.Mux{}
	mux.HandleFunc("/favicon.txt", processFavicon)
	mux.HandleFunc("/rebuild", processRebuild)
	// security
	// feed
	mux.HandleFunc("/", process)

	server := &gemini.Server{
		Addr:           ":" + port,
		Handler:        mux,
		ReadTimeout:    30 * time.Second,
		WriteTimeout:   1 * time.Minute,
		GetCertificate: certificates.Get,
	}

	ctx := context.Background()
	if err := server.ListenAndServe(ctx); err != nil {
		log.Fatal(err)
	}
}

func processRebuild(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
	log.Println("REBUILD")

	q, err := gemini.QueryUnescape(r.URL.RawQuery)
	if err != nil || q != key {
		w.WriteHeader(gemini.StatusServerUnavailable, "Don't wake up the dragons :-*")
	}

	conn, err := grpc.Dial(fileSrvHost+":"+fileSrvPort, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		log.Fatal(err)
		return
	}
	defer conn.Close()
	client := NewTreeManagerClient(conn)

	ts := TreeSecret{Key: key}
	rsp, err := client.RebuildTree(context.Background(), &ts)

	if rsp.Success {
		_, err = w.Write([]byte(":-)"))
		if err != nil {
			w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		}
		log.Println("REBUILD: success")
	} else {
		w.WriteHeader(gemini.StatusNotFound, "Out of space")
		log.Fatal(err)
	}
}

func processFavicon(_ context.Context, w gemini.ResponseWriter, _ *gemini.Request) {
	w.SetMediaType("text/plain")
	_, err := w.Write([]byte("\U0001F31B"))
	if err != nil {
		w.WriteHeader(gemini.StatusNotFound, "Out of space")
	}
}

func process(_ context.Context, w gemini.ResponseWriter, r *gemini.Request) {
	log.Println("-> " + r.URL.Path)

	lang := regexp.MustCompile(`^/(sgs|en)`).FindString(r.URL.Path)
	if lang != "" {
		lang = lang[1:]
	} else {
		lang = DefaultLang
	}

	conn, err := grpc.Dial(fileSrvHost+":"+fileSrvPort, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		log.Fatal(err)
		return
	}
	defer conn.Close()
	client := NewTreeManagerClient(conn)

	langs := strings.Join(SupportedLang, "|")
	switch {
	case "/" == r.URL.Path:
		w.WriteHeader(gemini.StatusPermanentRedirect, "/"+DefaultLang)
	case regexp.MustCompile(`^/(` + langs + `)/?$`).MatchString(r.URL.Path):
		renderIndex(lang, w, client)
	case regexp.MustCompile(`^/(` + langs + `)/atom.xml$`).MatchString(r.URL.Path):
		renderFeed(lang, w, r, client)
	case regexp.MustCompile(`^/(` + langs + `)/a/?$`).MatchString(r.URL.Path):
		renderAbout(lang, w)
	case regexp.MustCompile(`^/(` + langs + `)/s/?$`).MatchString(r.URL.Path):
		renderSearch(lang, w, r, client)
	case regexp.MustCompile(`^/(` + langs + `)/f/?$`).MatchString(r.URL.Path):
		renderAllFiles(lang, w, client)
	case regexp.MustCompile(`^/(` + langs + `)/f/([\p{L}\d_+.]+/)+[\d\w]+/index.gmi$`).MatchString(r.URL.Path):
		urlParts := strings.Split(r.URL.Path, "/")
		w.WriteHeader(gemini.StatusPermanentRedirect, strings.Join(urlParts[:(len(urlParts)-2)], "/"))
	case regexp.MustCompile(`^/(` + langs + `)/f/([\p{L}\d_+.]+/)+[\d\w]+/[\p{L}\d_+.]+.gmi$`).MatchString(r.URL.Path):
		renderFile(lang, w, r, client)
	case regexp.MustCompile(`^/(` + langs + `)/f/[\p{L}\d_+.]+(/[\p{L}\d_+.]+)*/?$`).MatchString(r.URL.Path):
		renderCategory(lang, w, r, client)
	case regexp.MustCompile(`^/(` + langs + `)/t/[\p{L}\d_+.]+/?$`).MatchString(r.URL.Path):
		renderTag(lang, w, r, client)
	case regexp.MustCompile(`^/__a/`).MatchString(r.URL.Path):
		downloadAttachment(w, r, client)
	default:
		w.WriteHeader(gemini.StatusNotFound, "Out of space")
	}

}

func downloadAttachment(w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
	urlParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
	filePath := urlParts[len(urlParts)-1]
	ar := AttachmentRequest{Path: filePath}
	response, err := client.DownloadAttachment(context.Background(), &ar)
	if err != nil {
		log.Fatalf("client.DownloadAttachment failed: %v", err)
		w.WriteHeader(gemini.StatusNotFound, "We lost it!")
		return
	}

	w.SetMediaType(mime.TypeByExtension(path.Ext(filePath)))
	for {
		chunkResponse, err := response.Recv()
		if err == io.EOF {
			log.Println("received all chunks")
			break
		}
		if err != nil {
			log.Println("err receiving chunk:", err)
			break
		}
		_, err = w.Write(chunkResponse.Chunk)
		if err != nil {
			w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
			return
		}
	}
}

func renderIndex(lang string, w gemini.ResponseWriter, client TreeManagerClient) {
	filters := []*TreeRequest_Filter{{Key: "lang", Value: lang}}
	if lang == "sgs" {
		for _, l := range []string{"lt", "prg", "eo"} {
			filters = append(filters, &TreeRequest_Filter{Key: "lang", Value: l})
		}
	}

	path := ""
	tr := TreeRequest{Path: &path, Filter: filters}

	tree, err := client.GetSummery(context.Background(), &tr)
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}

	w.SetMediaType("text/gemini")
	tpl := pongo2.Must(pongo2.FromFile(fmt.Sprintf("templates/%s/index.gmi", lang)))
	page, err := tpl.Execute(pongo2.Context{"tree": tree, "lang": lang, "lastFiles": GetLastFiles(tree.Files)})
	if err != nil {
		log.Fatalf("template failed: %v", err)
		return
	}

	_, err = w.Write([]byte(page))
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}
}

func renderFeed(lang string, w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
	langFilter := TreeRequest_Filter{Key: "lang", Value: lang}
	filters := []*TreeRequest_Filter{&langFilter}

	path := ""
	tr := TreeRequest{Path: &path, Filter: filters}

	tree, err := client.GetSummery(context.Background(), &tr)
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}

	w.SetMediaType("application/atom+xml")
	now := time.Now()
	language := "Samogitian"
	if lang == "en" {
		language = "English"
	}
	feed := &feeds.Feed{
		Title:       "\U0001F31B Arna alkierios [" + lang + "]",
		Link:        &feeds.Link{Href: "gemini://arns.lt/" + lang},
		Description: "personal gemini capsule by Arns Udovič [" + language + " version]",
		Author:      &feeds.Author{Name: "Arns Udovič", Email: "zordsdavini@arns.lt"},
		Created:     now,
	}
	for _, file := range GetLastFiles(tree.Files) {
		created, err := time.Parse("2006-01-02", file.Created)
		if err != nil {
			created = time.Now()
		}
		feed.Add(
			&feeds.Item{
				Title: file.Description,
				Link: &feeds.Link{Href: fmt.Sprintf("gemini://%s/%s/f%s/%s/%s",
					r.URL.Host,
					lang,
					file.CategoryPath(),
					file.Id,
					file.GmiName(),
				)},
				Description: fmt.Sprintf("%s %s [%s]",
					file.Description,
					file.CategoryPath(),
					strings.Join(file.Tags, ","),
				),
				Author:  &feeds.Author{Name: "Arns Udovič", Email: "zordsdavini@arns.lt"},
				Created: created,
			},
		)
	}

	atom, err := feed.ToAtom()
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}

	_, err = w.Write([]byte(atom))
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}
}

func renderAbout(lang string, w gemini.ResponseWriter) {
	w.SetMediaType("text/gemini")
	tpl := pongo2.Must(pongo2.FromFile(fmt.Sprintf("templates/%s/about.gmi", lang)))
	page, err := tpl.Execute(pongo2.Context{})
	if err != nil {
		log.Fatalf("template failed: %v", err)
		return
	}

	_, err = w.Write([]byte(page))
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
	}
}

func renderSearch(lang string, w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
	q, err := gemini.QueryUnescape(r.URL.RawQuery)
	if err != nil || q == "" {
		searchStr := "Input searching tag, created date or word in description"
		if lang == "sgs" {
			searchStr = "Ivesk ėiškuoma žīma, sokūrėma data arba žuodi ėš aprašīma"
		}
		w.WriteHeader(gemini.StatusInput, searchStr)
		return
	}

	descFilter := TreeRequest_Filter{Key: "description", Value: q}
	createdFilter := TreeRequest_Filter{Key: "created", Value: q}
	tagFilter := TreeRequest_Filter{Key: "tag", Value: q}
	filters := []*TreeRequest_Filter{&descFilter, &createdFilter, &tagFilter}

	path := ""
	tr := TreeRequest{Path: &path, Filter: filters}

	tree, err := client.GetSummery(context.Background(), &tr)
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}

	w.SetMediaType("text/gemini")
	tpl := pongo2.Must(pongo2.FromFile("templates/search.gmi"))
	page, err := tpl.Execute(pongo2.Context{"lang": lang, "tree": tree, "q": q})
	if err != nil {
		log.Fatalf("template failed: %v", err)
		return
	}

	_, err = w.Write([]byte(page))
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}
}

func renderCategory(lang string, w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
	urlParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
	path := "/" + strings.Join(urlParts[2:], "/")

	tr := TreeRequest{Path: &path, Filter: []*TreeRequest_Filter{}}

	tree, err := client.GetSummery(context.Background(), &tr)
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}

	indexFile, err := tree.GetIndexFile()
	var file *FileContent
	if err == nil {
		fr := FileRequest{Id: indexFile.Id}
		file, err = client.GetFile(context.Background(), &fr)
		if err != nil {
			w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
			return
		}
	}

	w.SetMediaType("text/gemini")
	tpl := pongo2.Must(pongo2.FromFile("templates/category.gmi"))
	page, err := tpl.Execute(pongo2.Context{"lang": lang, "tree": tree, "path": path, "indexFile": file})
	if err != nil {
		log.Fatalf("template failed: %v", err)
		return
	}

	_, err = w.Write([]byte(page))
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}
}

func renderFile(lang string, w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
	urlParts := strings.Split(r.URL.Path, "/")
	id := urlParts[len(urlParts)-2]

	fr := FileRequest{Id: id}
	file, err := client.GetFile(context.Background(), &fr)
	if err != nil {
		log.Fatalf("client.GetSummery failed: %v", err)
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}

	w.SetMediaType("text/gemini")
	tpl := pongo2.Must(pongo2.FromFile("templates/page.gmi"))
	page, err := tpl.Execute(pongo2.Context{"lang": lang, "file": file})
	if err != nil {
		log.Fatalf("template failed: %v", err)
		return
	}

	_, err = w.Write([]byte(page))
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}
}

func renderAllFiles(lang string, w gemini.ResponseWriter, client TreeManagerClient) {
	langFilter := TreeRequest_Filter{Key: "lang", Value: lang}
	filters := []*TreeRequest_Filter{&langFilter}

	path := ""
	tr := TreeRequest{Path: &path, Filter: filters}

	tree, err := client.GetSummery(context.Background(), &tr)
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}

	w.SetMediaType("text/gemini")
	tpl := pongo2.Must(pongo2.FromFile("templates/all_texts.gmi"))
	page, err := tpl.Execute(pongo2.Context{"lang": lang, "tree": tree})
	if err != nil {
		log.Fatalf("template failed: %v", err)
		return
	}

	_, err = w.Write([]byte(page))
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}
}

func renderTag(lang string, w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
	urlParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
	tag := urlParts[len(urlParts)-1]
	tagFilter := TreeRequest_Filter{Key: "tag", Value: tag}
	filters := []*TreeRequest_Filter{&tagFilter}

	path := ""
	tr := TreeRequest{Path: &path, Filter: filters}

	tree, err := client.GetSummery(context.Background(), &tr)
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}

	w.SetMediaType("text/gemini")
	tpl := pongo2.Must(pongo2.FromFile("templates/tag.gmi"))
	page, err := tpl.Execute(pongo2.Context{"lang": lang, "tree": tree, "tag": tag})
	if err != nil {
		log.Fatalf("template failed: %v", err)
		return
	}

	_, err = w.Write([]byte(page))
	if err != nil {
		w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
		return
	}
}