package notmain

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/letsencrypt/boulder/cmd"
	"github.com/letsencrypt/boulder/db"
	blog "github.com/letsencrypt/boulder/log"
	"github.com/letsencrypt/boulder/policy"
	"github.com/letsencrypt/boulder/sa"
)

type contactAuditor struct {
	db            *db.WrappedMap
	resultsFile   *os.File
	writeToStdout bool
	logger        blog.Logger
}

type result struct {
	id        int64
	contacts  []string
	createdAt string
}

func unmarshalContact(contact []byte) ([]string, error) {
	var contacts []string
	err := json.Unmarshal(contact, &contacts)
	if err != nil {
		return nil, err
	}
	return contacts, nil
}

func validateContacts(id int64, createdAt string, contacts []string) error {
	// Setup a buffer to store any validation problems we encounter.
	var probsBuff strings.Builder

	// Helper to write validation problems to our buffer.
	writeProb := func(contact string, prob string) {
		// Add validation problem to buffer.
		fmt.Fprintf(&probsBuff, "%d\t%s\tvalidation\t%q\t%q\t%q\n", id, createdAt, contact, prob, contacts)
	}

	for _, contact := range contacts {
		if strings.HasPrefix(contact, "mailto:") {
			err := policy.ValidEmail(strings.TrimPrefix(contact, "mailto:"))
			if err != nil {
				writeProb(contact, err.Error())
			}
		} else {
			writeProb(contact, "missing 'mailto:' prefix")
		}
	}

	if probsBuff.Len() != 0 {
		return errors.New(probsBuff.String())
	}
	return nil
}

// beginAuditQuery executes the audit query and returns a cursor used to
// stream the results.
func (c contactAuditor) beginAuditQuery(ctx context.Context) (*sql.Rows, error) {
	rows, err := c.db.QueryContext(ctx, `
		SELECT DISTINCT id, contact, createdAt
		FROM registrations
		WHERE contact NOT IN ('[]', 'null');`)
	if err != nil {
		return nil, err
	}
	return rows, nil
}

func (c contactAuditor) writeResults(result string) {
	if c.writeToStdout {
		_, err := fmt.Print(result)
		if err != nil {
			c.logger.Errf("Error while writing result to stdout: %s", err)
		}
	}

	if c.resultsFile != nil {
		_, err := c.resultsFile.WriteString(result)
		if err != nil {
			c.logger.Errf("Error while writing result to file: %s", err)
		}
	}
}

// run retrieves a cursor from `beginAuditQuery` and then audits the
// `contact` column of all returned rows for abnormalities or policy
// violations.
func (c contactAuditor) run(ctx context.Context, resChan chan *result) error {
	c.logger.Infof("Beginning database query")
	rows, err := c.beginAuditQuery(ctx)
	if err != nil {
		return err
	}

	for rows.Next() {
		var id int64
		var contact []byte
		var createdAt string
		err := rows.Scan(&id, &contact, &createdAt)
		if err != nil {
			return err
		}

		contacts, err := unmarshalContact(contact)
		if err != nil {
			c.writeResults(fmt.Sprintf("%d\t%s\tunmarshal\t%q\t%q\n", id, createdAt, contact, err))
		}

		err = validateContacts(id, createdAt, contacts)
		if err != nil {
			c.writeResults(err.Error())
		}

		// Only used for testing.
		if resChan != nil {
			resChan <- &result{id, contacts, createdAt}
		}
	}
	// Ensure the query wasn't interrupted before it could complete.
	err = rows.Close() //nolint:sqlclosecheck // the lint wants us to do this in a defer instead, but we want to return the error
	if err != nil {
		return err
	} else {
		c.logger.Info("Query completed successfully")
	}

	// Only used for testing.
	if resChan != nil {
		close(resChan)
	}

	return nil
}

type Config struct {
	ContactAuditor struct {
		DB cmd.DBConfig
	}
}

func main() {
	configFile := flag.String("config", "", "File containing a JSON config.")
	writeToStdout := flag.Bool("to-stdout", false, "Print the audit results to stdout.")
	writeToFile := flag.Bool("to-file", false, "Write the audit results to a file.")
	flag.Parse()

	logger := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 7})
	logger.Info(cmd.VersionString())

	if *configFile == "" {
		flag.Usage()
		os.Exit(1)
	}

	// Load config from JSON.
	configData, err := os.ReadFile(*configFile)
	cmd.FailOnError(err, fmt.Sprintf("Error reading config file: %q", *configFile))

	var cfg Config
	err = json.Unmarshal(configData, &cfg)
	cmd.FailOnError(err, "Couldn't unmarshal config")

	db, err := sa.InitWrappedDb(cfg.ContactAuditor.DB, nil, logger)
	cmd.FailOnError(err, "Couldn't setup database client")

	var resultsFile *os.File
	if *writeToFile {
		resultsFile, err = os.Create(
			fmt.Sprintf("contact-audit-%s.tsv", time.Now().Format("2006-01-02T15:04")),
		)
		cmd.FailOnError(err, "Failed to create results file")
	}

	// Setup and run contact-auditor.
	auditor := contactAuditor{
		db:            db,
		resultsFile:   resultsFile,
		writeToStdout: *writeToStdout,
		logger:        logger,
	}

	logger.Info("Running contact-auditor")

	err = auditor.run(context.TODO(), nil)
	cmd.FailOnError(err, "Audit was interrupted, results may be incomplete")

	logger.Info("Audit finished successfully")

	if *writeToFile {
		logger.Infof("Audit results were written to: %s", resultsFile.Name())
		resultsFile.Close()
	}

}

func init() {
	cmd.RegisterCommand("contact-auditor", main, &cmd.ConfigValidator{Config: &Config{}})
}
