/*
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 parser provides utilities for parsing of structured log messsages
// generated by maddy.
package parser

import (
	"encoding/json"
	"strings"
	"time"
	"unicode"
)

type (
	Msg struct {
		Stamp   time.Time
		Debug   bool
		Module  string
		Message string
		Context map[string]interface{}
	}

	MalformedMsg struct {
		Desc string
		Err  error
	}
)

const (
	ISO8601_UTC = "2006-01-02T15:04:05.000Z"
)

func (m MalformedMsg) Error() string {
	if m.Err != nil {
		return "parse: " + m.Desc + ": " + m.Err.Error()
	}
	return "parse: " + m.Desc
}

// Parse parses the message from the maddy log file.
//
// It assumes standard file output, including the [debug] tag and
// ISO 8601 timestamp at the start of each line. Timestamp is assumed to be in
// the UTC, as it is enforced by maddy.
//
// JSON context values are unmarshalled without any additional processing,
// notably that means that all numbers are represented as float64.
func Parse(line string) (Msg, error) {
	parts := strings.Split(line, "\t")
	if len(parts) != 2 {
		// All messages even without a Context have a trailing \t,
		// so this one is obviously malformed.
		return Msg{}, MalformedMsg{Desc: "missing a tab separator"}
	}

	m := Msg{
		Context: map[string]interface{}{},
	}

	// After that, the second part is the context. It can be empty, so don't fail
	// if there is none.
	if len(parts[1]) != 0 {
		if err := json.Unmarshal([]byte(parts[1]), &m.Context); err != nil {
			return Msg{}, MalformedMsg{Desc: "context unmarshal", Err: err}
		}
	}

	// Okay, the first one might contain the timestamp at start.
	// Cut it away.
	msgParts := strings.SplitN(parts[0], " ", 2)
	if len(msgParts) == 1 {
		return Msg{}, MalformedMsg{Desc: "missing a timestamp"}
	}

	var err error
	m.Stamp, err = time.ParseInLocation(ISO8601_UTC, msgParts[0], time.UTC)
	if err != nil {
		return Msg{}, MalformedMsg{Desc: "timestamp parse", Err: err}
	}

	msgText := msgParts[1]
	if strings.HasPrefix(msgText, "[debug] ") {
		msgText = strings.TrimPrefix(msgText, "[debug] ")
		m.Debug = true
	}

	moduleText := strings.SplitN(msgText, ": ", 2)
	if len(moduleText) == 1 {
		// No module prefix, that's fine.
		m.Message = msgText
		return m, nil
	}

	for _, ch := range moduleText[0] {
		switch {
		case unicode.IsDigit(ch), unicode.IsLetter(ch), ch == '/':
		default:
			// This is not a module prefix, don't treat it as such.
			m.Message = msgText
			return m, nil
		}
	}

	m.Module = moduleText[0]
	m.Message = moduleText[1]

	return m, nil
}
