// Copyright (C) 2023 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.

package monitor

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

var stdoutMu sync.Mutex

type notification interface {
	Environ() []string
	Summary() string
	Text() string
}

func notify(ctx context.Context, config *Config, notif notification) error {
	if config.Stdout {
		writeToStdout(notif)
	}

	if len(config.Email) > 0 {
		if err := sendEmail(ctx, config.Email, notif); err != nil {
			return err
		}
	}

	if config.Script != "" {
		if err := execScript(ctx, config.Script, notif); err != nil {
			return err
		}
	}

	if config.ScriptDir != "" {
		if err := execScriptDir(ctx, config.ScriptDir, notif); err != nil {
			return err
		}
	}

	return nil
}

func writeToStdout(notif notification) {
	stdoutMu.Lock()
	defer stdoutMu.Unlock()
	os.Stdout.WriteString(notif.Text() + "\n")
}

func sendEmail(ctx context.Context, to []string, notif notification) error {
	stdin := new(bytes.Buffer)
	stderr := new(bytes.Buffer)

	fmt.Fprintf(stdin, "To: %s\n", strings.Join(to, ", "))
	fmt.Fprintf(stdin, "Subject: [certspotter] %s\n", notif.Summary())
	fmt.Fprintf(stdin, "Date: %s\n", time.Now().Format(mailDateFormat))
	fmt.Fprintf(stdin, "Message-ID: <%s>\n", generateMessageID())
	fmt.Fprintf(stdin, "Mime-Version: 1.0\n")
	fmt.Fprintf(stdin, "Content-Type: text/plain; charset=US-ASCII\n")
	fmt.Fprintf(stdin, "X-Mailer: certspotter\n")
	fmt.Fprintf(stdin, "\n")
	fmt.Fprint(stdin, notif.Text())

	args := []string{"-i", "--"}
	args = append(args, to...)

	sendmail := exec.CommandContext(ctx, sendmailPath(), args...)
	sendmail.Stdin = stdin
	sendmail.Stderr = stderr

	if err := sendmail.Run(); err == nil {
		return nil
	} else if ctx.Err() != nil {
		return ctx.Err()
	} else if exitErr, isExitError := err.(*exec.ExitError); isExitError && exitErr.Exited() {
		return fmt.Errorf("error sending email to %v: sendmail failed with exit code %d and error %q", to, exitErr.ExitCode(), strings.TrimSpace(stderr.String()))
	} else {
		return fmt.Errorf("error sending email to %v: %w", to, err)
	}
}

func execScript(ctx context.Context, scriptName string, notif notification) error {
	stderr := new(bytes.Buffer)

	cmd := exec.CommandContext(ctx, scriptName)
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, notif.Environ()...)
	cmd.Stderr = stderr

	if err := cmd.Run(); err == nil {
		return nil
	} else if ctx.Err() != nil {
		return ctx.Err()
	} else if exitErr, isExitError := err.(*exec.ExitError); isExitError && exitErr.Exited() {
		return fmt.Errorf("script %q exited with code %d and error %q", scriptName, exitErr.ExitCode(), strings.TrimSpace(stderr.String()))
	} else if isExitError {
		return fmt.Errorf("script %q terminated by signal with error %q", scriptName, strings.TrimSpace(stderr.String()))
	} else {
		return fmt.Errorf("error executing script: %w", err)
	}
}

func execScriptDir(ctx context.Context, dirPath string, notif notification) error {
	dirents, err := os.ReadDir(dirPath)
	if errors.Is(err, fs.ErrNotExist) {
		return nil
	} else if err != nil {
		return fmt.Errorf("error executing scripts in directory %q: %w", dirPath, err)
	}
	for _, dirent := range dirents {
		if strings.HasPrefix(dirent.Name(), ".") {
			continue
		}
		scriptPath := filepath.Join(dirPath, dirent.Name())
		info, err := os.Stat(scriptPath)
		if errors.Is(err, fs.ErrNotExist) {
			continue
		} else if err != nil {
			return fmt.Errorf("error executing %q in directory %q: %w", dirent.Name(), dirPath, err)
		} else if info.Mode().IsRegular() && isExecutable(info.Mode()) {
			if err := execScript(ctx, scriptPath, notif); err != nil {
				return err
			}
		}
	}
	return nil
}

func isExecutable(mode os.FileMode) bool {
	return mode&0111 != 0
}
