//go:generate go install github.com/valyala/quicktemplate/qtc@latest
//go:generate qtc -dir=web/template
//go:generate go install golang.org/x/text/cmd/gotext@latest
//go:generate gotext -srclang=en update -out=catalog_gen.go -lang=en,ru
package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"io/fs"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"runtime/pprof"
	"strings"
	"syscall"
	"time"
	_ "time/tzdata"

	"github.com/caarlos0/env/v10"
	"golang.org/x/text/language"

	"source.toby3d.me/toby3d/home/internal/common"
	"source.toby3d.me/toby3d/home/internal/domain"
	"source.toby3d.me/toby3d/home/internal/page"
	pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs"
	pageucase "source.toby3d.me/toby3d/home/internal/page/usecase"
	sitefsrepo "source.toby3d.me/toby3d/home/internal/site/repository/fs"
	siteucase "source.toby3d.me/toby3d/home/internal/site/usecase"
	staticfsrepo "source.toby3d.me/toby3d/home/internal/static/repository/fs"
	staticucase "source.toby3d.me/toby3d/home/internal/static/usecase"
	"source.toby3d.me/toby3d/home/internal/templateutil"
	themefsrepo "source.toby3d.me/toby3d/home/internal/theme/repository/fs"
	themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase"
	"source.toby3d.me/toby3d/home/internal/urlutil"
)

type (
	App struct {
		server *http.Server
	}

	Context struct {
		Site *domain.Site
		Page *domain.Page
	}
)

var (
	config = new(domain.Config)
	logger = log.New(os.Stdout, "home\t", log.Lmsgprefix|log.LstdFlags|log.LUTC)
)

var cpuProfilePath, memProfilePath string

func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
	themeDir := os.DirFS(config.ThemeDir)
	contentDir := os.DirFS(config.ContentDir)
	resources := staticfsrepo.NewFileServerStaticRepository(contentDir)
	sites := sitefsrepo.NewFileSystemSiteRepository(contentDir)
	siter := siteucase.NewSiteUseCase(sites, resources)

	funcMap, err := templateutil.New(themeDir, siter)
	if err != nil {
		logger.Fatalln("cannot setup template.FuncMap for templates: %w", err)
	}

	staticDir := os.DirFS(config.StaticDir)
	statics := staticfsrepo.NewFileServerStaticRepository(staticDir)
	// TODO(toby3d): use exists static use case or split that on static and resource modules?
	resourcer := staticucase.NewStaticUseCase(resources)
	themes := themefsrepo.NewFileSystemThemeRepository(themeDir, funcMap)
	themer := themeucase.NewThemeUseCase(themes)
	pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
	pager := pageucase.NewPageUseCase(pages, resources)

	server := &http.Server{
		Addr: config.AddrPort().String(),
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// TODO(toby3d): use exists static use case or split that on static and resource modules?
			// INFO(toby3d): any static file is public and unprotected by design, so it's safe to search it
			// first before deep down to any page or it's resource which might be secured by middleware or
			// something else.
			static, err := statics.Get(r.Context(), strings.TrimPrefix(r.URL.Path, "/"))
			if err == nil {
				http.ServeContent(w, r, static.Name(), domain.ResourceModTime(static), static)

				return
			}

			lang := domain.LanguageUnd

			s, err := siter.Do(r.Context(), lang)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)

				return
			}

			if s.IsMultiLingual() {
				head, tail := urlutil.ShiftPath(r.URL.Path)

				if head == "" {
					supported := make([]language.Tag, len(s.Languages))
					for i := range s.Languages {
						supported[i] = language.Make(s.Languages[i].Lang())
					}

					if s.DefaultLanguage != domain.LanguageUnd {
						supported = append(
							[]language.Tag{language.Make(s.DefaultLanguage.Code())},
							supported...,
						)
					}

					requested, _, err := language.ParseAcceptLanguage(
						r.Header.Get(common.HeaderAcceptLanguage))
					if err != nil || len(requested) == 0 {
						requested = append(requested, language.English)
					}

					matched, _, _ := language.NewMatcher(supported).Match(requested...)
					lang = domain.NewLanguage(matched.String())

					http.Redirect(w, r, "/"+lang.Lang()+"/", http.StatusSeeOther)

					return
				}

				lang = domain.NewLanguage(head)
				r.URL.Path = tail
			}

			if s, err = siter.Do(r.Context(), lang); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)

				return
			}

			p, err := pager.Do(r.Context(), lang, r.URL.Path)
			if err != nil {
				if !errors.Is(err, page.ErrNotExist) {
					http.Error(w, err.Error(), http.StatusInternalServerError)

					return
				}

				res, err := resourcer.Do(r.Context(), r.URL.Path)
				if err != nil {
					if errors.Is(err, fs.ErrNotExist) {
						http.Error(w, err.Error(), http.StatusNotFound)

						return
					}

					http.Error(w, err.Error(), http.StatusInternalServerError)

					return
				}

				http.ServeContent(w, r, res.Name(), domain.ResourceModTime(res), res)

				return
			}

			contentLanguage := make([]string, len(p.Translations))
			for i := range p.Translations {
				contentLanguage[i] = p.Translations[i].Language.Code()
			}

			w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", "))

			tpl, err := themer.Do(r.Context())
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)

				return
			}

			w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
			if err = tpl.Execute(w, &Context{
				Site: s,
				Page: p,
			}); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
			}
		}),
		ErrorLog:     logger,
		BaseContext:  func(ln net.Listener) context.Context { return ctx },
		WriteTimeout: 500 * time.Millisecond,
	}

	return &App{server: server}, nil
}

func (a *App) Run(ln net.Listener) error {
	var err error
	if ln != nil {
		err = a.server.Serve(ln)
	} else {
		err = a.server.ListenAndServe()
	}

	if err != nil {
		return fmt.Errorf("cannot listen and serve server: %w", err)
	}

	return nil
}

func (a *App) Stop(ctx context.Context) error {
	if err := a.server.Shutdown(ctx); err != nil {
		return fmt.Errorf("failed to shutdown app: %w", err)
	}

	return nil
}

func main() {
	flag.StringVar(&cpuProfilePath, "cpuprofile", "", "set path to saving CPU memory profile")
	flag.StringVar(&memProfilePath, "memprofile", "", "set path to saving pprof memory profile")
	flag.Parse()

	var err error
	if err = env.ParseWithOptions(config, env.Options{Prefix: "HOME_"}); err != nil {
		logger.Fatalln("cannot unmarshal configuration into domain:", err)
	}

	for _, dir := range []*string{
		&config.ContentDir,
		&config.ThemeDir,
		&config.StaticDir,
	} {
		if *dir, err = filepath.Abs(filepath.Clean(*dir)); err != nil {
			logger.Fatalf("cannot format '%s' into absolute path: %s", *dir, err)
		}

		if _, err = os.Stat(*dir); err != nil {
			if errors.Is(err, os.ErrExist) {
				return
			}

			if !errors.Is(err, os.ErrNotExist) {
				logger.Fatalf("cannot check '%s' path: %v", *dir, err)
			}

			if err = os.MkdirAll(*dir, os.ModePerm); err != nil {
				logger.Fatalf("cannot create directory on '%s': %v", *dir, err)
			}
		}
	}

	ctx := context.Background()

	app, err := NewApp(ctx, config)
	if err != nil {
		logger.Fatalln("cannot setup app:", err)
	}

	done := make(chan os.Signal, 1)
	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)

	if cpuProfilePath != "" {
		cpuProfile, err := os.Create(cpuProfilePath)
		if err != nil {
			logger.Fatalln("could not create CPU profile:", err)
		}
		defer cpuProfile.Close()

		if err = pprof.StartCPUProfile(cpuProfile); err != nil {
			logger.Fatalln("could not start CPU profile:", err)
		}
		defer pprof.StopCPUProfile()
	}

	go func() {
		logger.Printf("starting server on %d...", config.Port)

		if err = app.Run(nil); err != nil {
			logger.Fatalln("cannot run app:", err)
		}
	}()

	<-done

	if err = app.Stop(ctx); err != nil {
		logger.Fatalln("failed shutdown server:", err)
	}

	if memProfilePath == "" {
		return
	}

	memProfile, err := os.Create(memProfilePath)
	if err != nil {
		logger.Fatalln("could not create memory profile:", err)
	}
	defer memProfile.Close()

	runtime.GC() // NOTE(toby3d): get up-to-date statistics

	if err = pprof.WriteHeapProfile(memProfile); err != nil {
		logger.Fatalln("could not write memory profile:", err)
	}
}
