package server

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"time"

	"go.alanpearce.eu/website/internal/builder"
	cfg "go.alanpearce.eu/website/internal/config"
	"go.alanpearce.eu/website/internal/vcs"
	"go.alanpearce.eu/website/internal/website"
	"go.alanpearce.eu/x/log"

	"github.com/ardanlabs/conf/v3"
	"github.com/osdevisnot/sorvor/pkg/livereload"
	"gitlab.com/tozd/go/errors"
)

var (
	CommitSHA    = "local"
	ShortSHA     = "local"
	serverHeader = fmt.Sprintf("website (%s)", ShortSHA)
)

type Config struct {
	Root          string `conf:"default:public"`
	Redirect      bool   `conf:"default:true"`
	ListenAddress string `conf:"default:localhost"`
	Port          int    `conf:"default:8080,short:p"`
	TLSPort       int    `conf:"default:8443"`
	TLS           bool   `conf:"default:false"`

	Development bool   `conf:"default:false,flag:dev"`
	ACMECA      string `conf:"env:ACME_CA"`
	ACMECACert  string `conf:"env:ACME_CA_CERT"`
	Domains     string
}

type Server struct {
	*http.Server
	runtimeConfig *Config
	config        *cfg.Config
	log           *log.Logger
}

func applyDevModeOverrides(config *cfg.Config, runtimeConfig *Config) {
	config.CSP.ScriptSrc = slices.Insert(config.CSP.ScriptSrc, 0, "'unsafe-inline'")
	config.CSP.ConnectSrc = slices.Insert(config.CSP.ConnectSrc, 0, "'self'")
	if runtimeConfig.Domains != "" {
		config.Domains = strings.Split(runtimeConfig.Domains, ",")
	} else {
		config.Domains = []string{runtimeConfig.ListenAddress}
	}
	scheme := "http"
	port := runtimeConfig.Port
	if runtimeConfig.TLS {
		scheme = "https"
		port = runtimeConfig.TLSPort
	}
	config.BaseURL = cfg.URL{
		URL: &url.URL{
			Scheme: scheme,
			Host:   net.JoinHostPort(config.Domains[0], strconv.Itoa(port)),
		},
	}
}

func updateCSPHashes(config *cfg.Config, r *builder.Result) {
	for i, h := range r.Hashes {
		config.CSP.StyleSrc[i] = fmt.Sprintf("'%s'", h)
	}
}

func serverHeaderHandler(wrappedHandler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Server", serverHeader)
		wrappedHandler.ServeHTTP(w, r)
	})
}

func rebuild(builderConfig *builder.IOConfig, config *cfg.Config, log *log.Logger) error {
	r, err := builder.BuildSite(builderConfig, config, log.Named("builder"))
	if err != nil {
		return errors.WithMessage(err, "could not build site")
	}
	updateCSPHashes(config, r)

	return nil
}

func New(runtimeConfig *Config, log *log.Logger) (*Server, error) {
	builderConfig := &builder.IOConfig{
		Destination: runtimeConfig.Root,
		Development: runtimeConfig.Development,
	}

	if !runtimeConfig.Development {
		vcsConfig := &vcs.Config{}
		_, err := conf.Parse("VCS", vcsConfig)
		if err != nil {
			return nil, err
		}
		if vcsConfig.LocalPath != "" {
			_, err = vcs.CloneOrUpdate(vcsConfig, log.Named("vcs"))
			if err != nil {
				return nil, err
			}
			err = os.Chdir(runtimeConfig.Root)
			if err != nil {
				return nil, err
			}

			builderConfig.Source = vcsConfig.LocalPath

			publicDir := filepath.Join(runtimeConfig.Root, "public")
			builderConfig.Destination = publicDir
			runtimeConfig.Root = publicDir
		} else {
			log.Warn("in production mode without VCS configuration")
		}
	}

	config, err := cfg.GetConfig(builderConfig.Source, log.Named("config"))
	if err != nil {
		return nil, errors.WithMessage(err, "error parsing configuration file")
	}
	if runtimeConfig.Development {
		applyDevModeOverrides(config, runtimeConfig)
	}

	top := http.NewServeMux()

	err = rebuild(builderConfig, config, log)
	if err != nil {
		return nil, err
	}

	fixupMIMETypes(log)

	if runtimeConfig.Development {
		liveReload := livereload.New()
		top.Handle("/_/reload", liveReload)
		liveReload.Start()
		fw, err := NewFileWatcher(log.Named("watcher"))
		if err != nil {
			return nil, errors.WithMessage(err, "could not create file watcher")
		}
		for _, dir := range []string{"content", "static", "templates", "internal/builder"} {
			err := fw.AddRecursive(dir)
			if err != nil {
				return nil, errors.WithMessagef(
					err,
					"could not add directory %s to file watcher",
					dir,
				)
			}
		}
		err = fw.Add(".")
		if err != nil {
			return nil, errors.WithMessage(err, "could not add directory to file watcher")
		}
		go fw.Start(func(filename string) {
			log.Info("rebuilding site", "changed_file", filename)
			err := rebuild(builderConfig, config, log)
			if err != nil {
				log.Error("error rebuilding site", "error", err)
			}
		})
	}

	loggingMux := http.NewServeMux()
	mux, err := website.NewMux(config, runtimeConfig.Root, log.Named("website"))
	if err != nil {
		return nil, errors.WithMessage(err, "could not create website mux")
	}

	if runtimeConfig.Redirect {
		re := regexp.MustCompile(
			"^(.*)\\." + strings.ReplaceAll(config.WildcardDomain, ".", `\.`) + "$",
		)
		replace := "${1}." + config.Domains[0]
		loggingMux.Handle(config.BaseURL.Hostname()+"/", mux)
		loggingMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
			if slices.Contains(config.Domains, r.Host) {
				path, _ := website.CanonicalisePath(r.URL.Path)
				newURL := config.BaseURL.JoinPath(path)
				http.Redirect(w, r, newURL.String(), http.StatusMovedPermanently)
			} else if re.MatchString(r.Host) {
				url := config.BaseURL
				url.Host = re.ReplaceAllString(r.Host, replace)
				http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect)
			} else {
				http.NotFound(w, r)
			}
		})
	} else {
		loggingMux.Handle("/", mux)
	}

	top.Handle("/",
		serverHeaderHandler(
			wrapHandlerWithLogging(loggingMux, log),
		),
	)

	top.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNoContent)
	})

	return &Server{
		Server: &http.Server{
			ReadHeaderTimeout: 10 * time.Second,
			ReadTimeout:       1 * time.Minute,
			WriteTimeout:      2 * time.Minute,
			IdleTimeout:       10 * time.Minute,
			Handler:           top,
		},
		log:           log,
		config:        config,
		runtimeConfig: runtimeConfig,
	}, nil
}

func (s *Server) serve(tls bool) error {
	if tls {
		return s.serveTLS()
	}

	return s.serveTCP()
}

func (s *Server) Start() error {
	if err := s.serve(s.runtimeConfig.TLS); err != http.ErrServerClosed {
		return errors.WithMessage(err, "error creating/closing server")
	}

	return nil
}

func (s *Server) Stop() chan struct{} {
	s.log.Debug("stop called")

	idleConnsClosed := make(chan struct{})

	go func() {
		s.log.Debug("shutting down server")
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		err := s.Server.Shutdown(ctx)
		s.log.Debug("server shut down")
		if err != nil {
			// Error from closing listeners, or context timeout:
			s.log.Warn("HTTP server Shutdown", "error", err)
		}
		close(idleConnsClosed)
	}()

	return idleConnsClosed
}