// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package caddy

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"runtime/debug"
	"strings"
	"sync"
	"time"

	"github.com/mholt/certmagic"
)

// Config represents a Caddy configuration.
type Config struct {
	Admin      *AdminConfig               `json:"admin,omitempty"`
	Logging    *Logging                   `json:"logging,omitempty"`
	StorageRaw json.RawMessage            `json:"storage,omitempty"`
	AppsRaw    map[string]json.RawMessage `json:"apps,omitempty"`

	apps    map[string]App
	storage certmagic.Storage

	cancelFunc context.CancelFunc
}

// App is a thing that Caddy runs.
type App interface {
	Start() error
	Stop() error
}

// Run runs Caddy with the given config.
func Run(newCfg *Config) error {
	currentCfgMu.Lock()
	defer currentCfgMu.Unlock()

	// run the new config and start all its apps
	err := run(newCfg, true)
	if err != nil {
		return err
	}

	// swap old config with the new one
	oldCfg := currentCfg
	currentCfg = newCfg

	// Stop, Cleanup each old app
	unsyncedStop(oldCfg)

	return nil
}

// run runs newCfg and starts all its apps if
// start is true. If any errors happen, cleanup
// is performed if any modules were provisioned;
// apps that were started already will be stopped,
// so this function should not leak resources if
// an error is returned. However, if no error is
// returned and start == false, you should cancel
// the config if you are not going to start it,
// so that each provisioned module will be
// cleaned up.
func run(newCfg *Config, start bool) error {
	if newCfg == nil {
		return nil
	}

	// because we will need to roll back any state
	// modifications if this function errors, we
	// keep a single error value and scope all
	// sub-operations to their own functions to
	// ensure this error value does not get
	// overridden or missed when it should have
	// been set by a short assignment
	var err error

	// prepare the new config for use
	newCfg.apps = make(map[string]App)

	// create a context within which to load
	// modules - essentially our new config's
	// execution environment; be sure that
	// cleanup occurs when we return if there
	// was an error; if no error, it will get
	// cleaned up on next config cycle
	ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
	defer func() {
		if err != nil {
			// if there were any errors during startup,
			// we should cancel the new context we created
			// since the associated config won't be used;
			// this will cause all modules that were newly
			// provisioned to clean themselves up
			cancel()

			// also undo any other state changes we made
			if currentCfg != nil {
				certmagic.Default.Storage = currentCfg.storage
			}
		}
	}()
	newCfg.cancelFunc = cancel // clean up later

	// set up logging before anything bad happens
	if newCfg.Logging == nil {
		newCfg.Logging = new(Logging)
	}
	err = newCfg.Logging.openLogs(ctx)
	if err != nil {
		return err
	}

	// set up global storage and make it CertMagic's default storage, too
	err = func() error {
		if newCfg.StorageRaw != nil {
			val, err := ctx.LoadModuleInline("module", "caddy.storage", newCfg.StorageRaw)
			if err != nil {
				return fmt.Errorf("loading storage module: %v", err)
			}
			stor, err := val.(StorageConverter).CertMagicStorage()
			if err != nil {
				return fmt.Errorf("creating storage value: %v", err)
			}
			newCfg.storage = stor
			newCfg.StorageRaw = nil // allow GC to deallocate
		}
		if newCfg.storage == nil {
			newCfg.storage = &certmagic.FileStorage{Path: dataDir()}
		}
		certmagic.Default.Storage = newCfg.storage

		return nil
	}()
	if err != nil {
		return err
	}

	// Load, Provision, Validate each app and their submodules
	err = func() error {
		for modName, rawMsg := range newCfg.AppsRaw {
			val, err := ctx.LoadModule(modName, rawMsg)
			if err != nil {
				return fmt.Errorf("loading app module '%s': %v", modName, err)
			}
			newCfg.apps[modName] = val.(App)
		}
		return nil
	}()
	if err != nil {
		return err
	}

	if !start {
		return nil
	}

	// Start
	return func() error {
		var started []string
		for name, a := range newCfg.apps {
			err := a.Start()
			if err != nil {
				// an app failed to start, so we need to stop
				// all other apps that were already started
				for _, otherAppName := range started {
					err2 := newCfg.apps[otherAppName].Stop()
					if err2 != nil {
						err = fmt.Errorf("%v; additionally, aborting app %s: %v",
							err, otherAppName, err2)
					}
				}
				return fmt.Errorf("%s app module: start: %v", name, err)
			}
			started = append(started, name)
		}
		return nil
	}()
}

// Stop stops running the current configuration.
// It is the antithesis of Run(). This function
// will log any errors that occur during the
// stopping of individual apps and continue to
// stop the others.
func Stop() error {
	currentCfgMu.Lock()
	defer currentCfgMu.Unlock()
	unsyncedStop(currentCfg)
	currentCfg = nil
	return nil
}

// unsyncedStop stops cfg from running, but if
// applicable, you need to acquire locks yourself.
// It is a no-op if cfg is nil. If any app
// returns an error when stopping, it is logged
// and the function continues with the next app.
// This function assumes all apps in cfg were
// successfully started.
func unsyncedStop(cfg *Config) {
	if cfg == nil {
		return
	}

	// stop each app
	for name, a := range cfg.apps {
		err := a.Stop()
		if err != nil {
			log.Printf("[ERROR] stop %s: %v", name, err)
		}
	}

	// clean up all modules
	cfg.cancelFunc()
}

// Validate loads, provisions, and validates
// cfg, but does not start running it.
func Validate(cfg *Config) error {
	err := run(cfg, false)
	if err == nil {
		cfg.cancelFunc() // call Cleanup on all modules
	}
	return err
}

// Duration is a JSON-string-unmarshable duration type.
type Duration time.Duration

// UnmarshalJSON satisfies json.Unmarshaler.
func (d *Duration) UnmarshalJSON(b []byte) error {
	dd, err := time.ParseDuration(strings.Trim(string(b), `"`))
	if err != nil {
		return err
	}
	*d = Duration(dd)
	return nil
}

// GoModule returns the build info of this Caddy
// build from debug.BuildInfo (requires Go modules).
// If no version information is available, a non-nil
// value will still be returned, but with an
// unknown version.
func GoModule() *debug.Module {
	var mod debug.Module
	return goModule(&mod)
}

// goModule holds the actual implementation of GoModule.
// Allocating debug.Module in GoModule() and passing a
// reference to goModule enables mid-stack inlining.
func goModule(mod *debug.Module) *debug.Module {
	mod.Version = "unknown"
	bi, ok := debug.ReadBuildInfo()
	if ok {
		mod.Path = bi.Main.Path
		// The recommended way to build Caddy involves
		// creating a separate main module, which
		// TODO: track related Go issue: https://github.com/golang/go/issues/29228
		// once that issue is fixed, we should just be able to use bi.Main... hopefully.
		for _, dep := range bi.Deps {
			if dep.Path == "github.com/caddyserver/caddy/v2" {
				return dep
			}
		}
		return &bi.Main
	}
	return mod
}

// CtxKey is a value type for use with context.WithValue.
type CtxKey string

// currentCfg is the currently-loaded configuration.
var (
	currentCfg   *Config
	currentCfgMu sync.RWMutex
)