package server

import (
	"database/sql"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"syscall"

	"html/template"
	"lieu/database"
	"lieu/types"
	"lieu/util"
)

type RequestHandler struct {
	config types.Config
	db     *sql.DB
}

type TemplateView struct {
	SiteName string
	Data     interface{}
}

type SearchData struct {
	Query      string
	Title      string
	Site       string
	Pages      []types.PageData
	IsInternal bool
}

type IndexData struct {
	Tagline     string
	Placeholder string
}

type ListData struct {
	Title string
	URLs  []types.PageData
}

type AboutData struct {
	DomainCount  int
	WebringName  string
	LastCrawl    string
	PageCount    string
	TermCount    string
	FilteredLink string
	RingLink     string
}

var templates = template.Must(template.ParseFiles(
	"html/head.html", "html/nav.html", "html/footer.html",
	"html/about.html", "html/index.html", "html/list.html", "html/search.html", "html/webring.html"))

const useURLTitles = true

func (h RequestHandler) searchRoute(res http.ResponseWriter, req *http.Request) {
	var query string
	var domain string
	view := &TemplateView{}

	var domains = []string{}
	var nodomains = []string{}
	var langs = []string{}
	var queryFields = []string{}
		
	if req.Method == http.MethodGet{
		params := req.URL.Query()
		if words, exists := params["q"]; exists && words[0] != "" {
			query = words[0]
			queryFields = strings.Fields(query)
		}

		// how to use: https://gist.github.com/cblgh/29991ba0a9e65cccbe14f4afd7c975f1
		if parts, exists := params["site"]; exists && parts[0] != "" {
			// make sure we only have the domain, and no protocol prefix
			domain = strings.TrimPrefix(parts[0], "https://")
			domain = strings.TrimPrefix(domain, "http://")
			domain = strings.TrimSuffix(domain, "/")
			domains = append(domains, domain)
		}

		// don't process if there are too many fields
		if len(queryFields) <= 100 {
			var newQueryFields []string;
			for _, word := range queryFields {
				// This could be more efficient by splitting arrays, but I'm going with the more readable version for now
				if strings.HasPrefix(word, "site:") {
					domains = append(domains, strings.TrimPrefix(word, "site:"))
				} else if strings.HasPrefix(word, "-site:") {
					nodomains = append(nodomains, strings.TrimPrefix(word, "-site:"))
				} else if strings.HasPrefix(word, "lang:") {
					langs = append(langs, strings.TrimPrefix(word, "lang:"))
				} else {
					newQueryFields = append(newQueryFields, word)
				}
			}
			queryFields = newQueryFields;
		}
		
	}

	if len(queryFields) == 0 || len(queryFields) > 100 || len(query) >= 8192 {
		view.Data = IndexData{Tagline: h.config.General.Tagline, Placeholder: h.config.General.Placeholder}
		h.renderView(res, "index", view)
		return
	}

	var pages = database.SearchWords(h.db, util.Inflect(queryFields), true, domains, nodomains, langs)

	if useURLTitles {
		for i, pageData := range pages {
			prettyURL, err := url.QueryUnescape(strings.TrimPrefix(strings.TrimPrefix(pageData.URL, "http://"), "https://"))
			util.Check(err)
			pageData.Title = prettyURL
			pages[i] = pageData
		}
	}

	view.Data = SearchData{
		Title:      "Results",
		Query:      query,
		Site:       domain,
		Pages:      pages,
		IsInternal: true,
	}
	h.renderView(res, "search", view)
}

func (h RequestHandler) externalSearchRoute(res http.ResponseWriter, req *http.Request) {
	var query string
	view := &TemplateView{}

	if req.Method == http.MethodGet {
		params := req.URL.Query()
		if words, exists := params["q"]; exists && words[0] != "" {
			query = words[0]
		}
	}

	pages := database.FulltextSearchWords(h.db, query)

	if useURLTitles {
		for i, pageData := range pages {
			prettyURL, err := url.QueryUnescape(strings.TrimPrefix(strings.TrimPrefix(pageData.URL, "http://"), "https://"))
			util.Check(err)
			pageData.Title = prettyURL
			pages[i] = pageData
		}
	}

	view.Data = SearchData{
		Title:      "External Results",
		Query:      query,
		Pages:      pages,
		IsInternal: false,
	}
	h.renderView(res, "search", view)
}

func (h RequestHandler) aboutRoute(res http.ResponseWriter, req *http.Request) {
	view := &TemplateView{}

	pageCount := util.Humanize(database.GetPageCount(h.db))
	wordCount := util.Humanize(database.GetWordCount(h.db))
	domainCount := database.GetDomainCount(h.db)
	lastCrawl := database.GetLastCrawl(h.db)

	view.Data = AboutData{
		WebringName:  h.config.General.Name,
		DomainCount:  domainCount,
		PageCount:    pageCount,
		TermCount:    wordCount,
		LastCrawl:    lastCrawl,
		FilteredLink: "/filtered",
		RingLink:     h.config.General.URL,
	}
	h.renderView(res, "about", view)
}

func (h RequestHandler) filteredRoute(res http.ResponseWriter, req *http.Request) {
	view := &TemplateView{}

	var URLs []types.PageData
	for _, domain := range util.ReadList(h.config.Crawler.BannedDomains, "\n") {
		u, err := url.Parse(domain)
		if err != nil {
			continue
		}
		u.Scheme = "https"
		p := types.PageData{Title: domain, URL: u.String()}
		URLs = append(URLs, p)
	}

	view.Data = ListData{
		Title: "Filtered Domains",
		URLs:  URLs,
	}
	h.renderView(res, "list", view)
}

func (h RequestHandler) randomRoute(res http.ResponseWriter, req *http.Request) {
	link := database.GetRandomPage(h.db)
	http.Redirect(res, req, link, http.StatusSeeOther)
}

func (h RequestHandler) randomExternalRoute(res http.ResponseWriter, req *http.Request) {
	link := database.GetRandomExternalLink(h.db)
	http.Redirect(res, req, link, http.StatusSeeOther)
}

func (h RequestHandler) webringRoute(res http.ResponseWriter, req *http.Request) {
	http.Redirect(res, req, h.config.General.URL, http.StatusSeeOther)
}

func (h RequestHandler) renderView(res http.ResponseWriter, tmpl string, view *TemplateView) {
	view.SiteName = h.config.General.Name
	var errTemp error
	if _, exists := os.LookupEnv("LIEU_DEV"); exists {
		var templates = template.Must(template.ParseFiles(
			"html/head.html", "html/nav.html", "html/footer.html",
			"html/about.html", "html/index.html", "html/list.html", "html/search.html", "html/webring.html"))
		errTemp = templates.ExecuteTemplate(res, tmpl+".html", view)
	} else {
		errTemp = templates.ExecuteTemplate(res, tmpl+".html", view)
	}
	if errors.Is(errTemp, syscall.EPIPE) {
		fmt.Println("had a broken pipe, continuing")
	} else {
		util.Check(errTemp)
	}
}

func WriteTheme(config types.Config) {
	theme := config.Theme
	// no theme is set, use the default
	if theme.Foreground == "" || theme.Background == "" || theme.Links =="" {
		return
	}
	colors := fmt.Sprintf(`/*This file will be automatically regenerated by lieu on startup if the theme colors are set in the configuration file*/
:root {
  --primary: %s;
  --secondary: %s;
  --link: %s;
}`, theme.Foreground, theme.Background, theme.Links)
	err := os.WriteFile("html/assets/theme.css", []byte(colors), 0644)
	util.Check(err)
}

func Serve(config types.Config) {
	WriteTheme(config)
	db := database.InitDB(config.Data.Database)
	handler := RequestHandler{config: config, db: db}

	http.HandleFunc("/about", handler.aboutRoute)
	http.HandleFunc("/", handler.searchRoute)
	http.HandleFunc("/outgoing", handler.externalSearchRoute)
	http.HandleFunc("/random/outgoing", handler.randomExternalRoute)
	http.HandleFunc("/random", handler.randomRoute)
	http.HandleFunc("/webring", handler.webringRoute)
	http.HandleFunc("/filtered", handler.filteredRoute)

	fileserver := http.FileServer(http.Dir("html/"))
	http.Handle("/assets/", fileserver)
	http.Handle("/robots.txt", fileserver)

	portstr := fmt.Sprintf(":%d", config.General.Port)
	fmt.Println("Listening on port: ", portstr)

	http.ListenAndServe(portstr, nil)
}
