package web

import (
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"log"
	"net/http"
	_ "net/http/pprof"
	"os"
	"path/filepath"
	"strings"

	"golang.org/x/crypto/acme/autocert"
	"gordenko.dev/dima/web/view"
)

const defaultTemplateExt = "html"

type _StaticDir struct {
	Prefix string
	Dir    string
}

type FileSystem struct {
	FileSystem http.FileSystem
	Prefix     string
}

type EmbedFS struct {
	Dir string
	FS  fs.FS
}

type AppOptions struct {
	Logger            *log.Logger
	Address           string
	Port              int
	TLSConfig         *tls.Config
	BaseDir           string
	StaticDirs        map[string]string
	TemplateDir       string
	Router            Router
	UploadTempDir     string
	Renderer          Renderer
	PublicKey         string
	PrivateKey        string
	LetsEncryptDomain string
	StaticFSs         []EmbedFS
	TemplateFS        *EmbedFS
}

type App struct {
	logger            *log.Logger
	address           string
	port              int
	tlsConfig         *tls.Config
	staticDirs        []_StaticDir
	router            Router
	renderer          Renderer
	baseDir           string
	templateDir       string
	templateFS        *EmbedFS
	uploadTempDir     string
	publicKey         string
	privateKey        string
	letsEncryptDomain string
	staticFSs         []EmbedFS
}

func NewApp(opt AppOptions) (*App, error) {
	if opt.Router == nil {
		return nil, errors.New("Router option is required")
	}
	if opt.Logger == nil {
		return nil, errors.New("Logger option is required")
	}
	s := &App{
		logger:            opt.Logger,
		router:            opt.Router,
		address:           opt.Address,
		port:              80,
		publicKey:         opt.PublicKey,
		privateKey:        opt.PrivateKey,
		letsEncryptDomain: opt.LetsEncryptDomain,
		staticFSs:         opt.StaticFSs,
	}
	if opt.Port > 0 {
		s.port = opt.Port
	}
	s.uploadTempDir = s.path(opt.UploadTempDir)

	var err error

	if opt.BaseDir != "" {
		s.baseDir = opt.BaseDir
	} else {
		s.baseDir, err = os.Getwd()
		if err != nil {
			return nil, fmt.Errorf("BaseDir is not set, os.Getwd: %s", err)
		}
	}

	if opt.TemplateFS != nil {
		s.templateFS = opt.TemplateFS
	} else {
		s.templateDir = s.path(opt.TemplateDir)
	}

	for prefix, dir := range opt.StaticDirs {
		if !strings.HasPrefix(prefix, "/") {
			prefix = "/" + prefix
		}
		if !strings.HasSuffix(prefix, "/") {
			prefix += "/"
		}
		s.staticDirs = append(s.staticDirs, _StaticDir{
			Prefix: prefix,
			Dir:    s.path(dir),
		})
	}

	for _, item := range opt.StaticFSs {
		if strings.ContainsAny(item.Dir, "/\\") {
			return nil, fmt.Errorf("Invalid Dir: %s", item.Dir)
		}
	}

	if opt.Renderer != nil {
		s.renderer = opt.Renderer
	} else {
		if s.templateDir != "" {
			s.renderer, err = view.NewTemplateNoCache(s.templateDir, defaultTemplateExt)
			if err != nil {
				return nil, fmt.Errorf("view.NewTemplateNoCache: %s", err)
			}
		} else if s.templateFS != nil {
			s.renderer, err = view.NewTemplateInFS(s.templateFS.FS, s.templateFS.Dir, defaultTemplateExt)
			if err != nil {
				return nil, fmt.Errorf("view.NewTemplateInFS: %s", err)
			}
		}
	}
	return s, nil
}

func (s *App) path(dir string) string {
	if filepath.IsAbs(dir) {
		return dir
	}
	return filepath.Join(s.baseDir, dir)
}

func (s *App) Run() {
	var (
		err error
		mux = http.NewServeMux()
	)
	for _, item := range s.staticDirs {
		mux.Handle(item.Prefix,
			http.StripPrefix(item.Prefix, http.FileServer(http.Dir(item.Dir))))
	}
	for _, item := range s.staticFSs {
		mux.Handle("/"+item.Dir+"/", http.FileServer(http.FS(item.FS)))
	}
	mux.Handle("/", s)
	if s.letsEncryptDomain != "" {
		err = http.Serve(autocert.NewListener(s.letsEncryptDomain), mux)
		if err != nil {
			log.Fatalln(err)
		}
	} else {
		srv := &http.Server{
			Addr:    fmt.Sprintf("%s:%d", s.address, s.port),
			Handler: mux,
		}
		if s.publicKey != "" && s.privateKey != "" {
			if s.tlsConfig != nil {
				srv.TLSConfig = s.tlsConfig
			}
			err = srv.ListenAndServeTLS(s.publicKey, s.privateKey)
			if err != nil {
				log.Fatalln(err)
			}
		} else {
			err = srv.ListenAndServe()
			if err != nil {
				log.Fatalln(err)
			}
		}
	}
}

func (s *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	path := r.URL.Path
	if path != "/" && strings.HasSuffix(path, "/") {
		http.Redirect(w, r, strings.TrimRight(path, "/"), http.StatusSeeOther)
		return
	}
	wrappers, handleFunc := s.router.Find(path)
	if handleFunc == nil {
		s.logger.Printf("router.Find: path '%s' not found\n", path)
		http.NotFound(w, r)
		return
	}

	reply := NewReply(s.renderer)
	reply.Code = 200
	reply.SetHeader("Content-Type", "text/html; charset=utf-8")

	state := &State{
		Header:        r.Header,
		w:             w,
		r:             r,
		reply:         reply,
		wrappers:      wrappers,
		handler:       handleFunc,
		uploadTempDir: s.uploadTempDir,
	}
	err := state.Next()
	if err != nil {
		s.logger.Printf("state.Next: %s\n", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	for k, v := range reply.headers {
		w.Header().Set(k, v)
	}
	for _, cookie := range reply.cookies {
		http.SetCookie(w, cookie)
	}
	w.WriteHeader(reply.Code)
	_, err = io.Copy(w, reply.buffer)
	if err != nil {
		s.logger.Printf("Write reply: %s\n", err)
	}
}
