/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

package tls

import (
	"crypto/tls"
	"errors"
	"fmt"
	"path/filepath"
	"sync"
	"time"

	"github.com/foxcpp/maddy/framework/config"
	"github.com/foxcpp/maddy/framework/container"
	"github.com/foxcpp/maddy/framework/log"
	"github.com/foxcpp/maddy/framework/module"
	"github.com/foxcpp/maddy/framework/module/modules"
)

type FileLoader struct {
	instName  string
	certPaths []string
	keyPaths  []string
	log       *log.Logger

	certs     []tls.Certificate
	certsLock sync.RWMutex

	reloadTick *time.Ticker
	stopTick   chan struct{}
}

func NewFileLoader(c *container.C, modName, instName string) (module.Module, error) {
	return &FileLoader{
		instName: instName,
		log:      c.DefaultLogger.Sublogger(modName),
		stopTick: make(chan struct{}),
	}, nil
}

func (f *FileLoader) Configure(inlineArgs []string, cfg *config.Map) error {
	cfg.StringList("certs", false, false, nil, &f.certPaths)
	cfg.StringList("keys", false, false, nil, &f.keyPaths)
	if _, err := cfg.Process(); err != nil {
		return err
	}

	if len(f.certPaths) != len(f.keyPaths) {
		return errors.New("tls.loader.file: mismatch in certs and keys count")
	}

	if len(inlineArgs)%2 != 0 {
		return errors.New("tls.loader.file: odd amount of arguments")
	}
	for i := 0; i < len(inlineArgs); i += 2 {
		f.certPaths = append(f.certPaths, inlineArgs[i])
		f.keyPaths = append(f.keyPaths, inlineArgs[i+1])
	}

	for _, certPath := range f.certPaths {
		if !filepath.IsAbs(certPath) {
			return fmt.Errorf("tls.loader.file: only absolute paths allowed in certificate paths: sorry :(")
		}
	}

	if err := f.loadCerts(); err != nil {
		return err
	}

	return nil
}

func (f *FileLoader) Start() error {
	f.reloadTick = time.NewTicker(time.Minute)
	go f.reloadTicker()
	return nil
}

func (f *FileLoader) Reload() error {
	f.log.Println("reloading certificates")
	return f.loadCerts()
}

func (f *FileLoader) Stop() error {
	f.reloadTick.Stop()
	f.stopTick <- struct{}{}
	return nil
}

func (f *FileLoader) Name() string {
	return "tls.loader.file"
}

func (f *FileLoader) InstanceName() string {
	return f.instName
}

func (f *FileLoader) reloadTicker() {
	for {
		select {
		case <-f.reloadTick.C:
			f.log.Debugln("reloading certs")
			if err := f.loadCerts(); err != nil {
				f.log.Error("reload failed", err)
			}
		case <-f.stopTick:
			return
		}
	}
}

func (f *FileLoader) loadCerts() error {
	if len(f.certPaths) != len(f.keyPaths) {
		return errors.New("mismatch in certs and keys count")
	}

	if len(f.certPaths) == 0 {
		return errors.New("tls.loader.file: at least one certificate required")
	}

	certs := make([]tls.Certificate, 0, len(f.certPaths))

	for i := range f.certPaths {
		certPath := f.certPaths[i]
		keyPath := f.keyPaths[i]

		cert, err := tls.LoadX509KeyPair(certPath, keyPath)
		if err != nil {
			return fmt.Errorf("failed to load %s and %s: %v", certPath, keyPath, err)
		}
		certs = append(certs, cert)
	}

	f.certsLock.Lock()
	defer f.certsLock.Unlock()
	f.certs = certs

	return nil
}

func (f *FileLoader) ConfigureTLS(c *tls.Config) error {
	// Loader function replaces only the whole slice.
	f.certsLock.RLock()
	defer f.certsLock.RUnlock()

	c.Certificates = f.certs
	return nil
}

func init() {
	var _ module.TLSLoader = &FileLoader{}
	modules.Register("tls.loader.file", NewFileLoader)
}
