package accesslog

import (
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"sync"
	"text/template"
	"time"

	"github.com/kataras/iris/v12/context"
	"github.com/kataras/iris/v12/core/memstore"
)

// Log represents the log data specifically for the accesslog middleware.
type Log struct {
	// The AccessLog instance this Log was created of.
	Logger *AccessLog `json:"-" yaml:"-" toml:"-"`

	// The time the log is created.
	Now time.Time `json:"-" yaml:"-" toml:"-"`
	// TimeFormat selected to print the Time as string,
	// useful on Template Formatter.
	TimeFormat string `json:"-" yaml:"-" toml:"-"`
	// Timestamp the Now's unix timestamp (seconds).
	Timestamp int64 `json:"timestamp"`

	// Request-Response latency.
	Latency time.Duration `json:"latency"`
	// Init request's Method and Path.
	Method string `json:"method"`
	Path   string `json:"path"`
	// The response status code.
	Code int `json:"code"`
	// Sorted URL Query arguments.
	Query []memstore.StringEntry `json:"query"`
	// Dynamic path parameters.
	PathParams []memstore.Entry `json:"params"`
	// Fields any data information useful to represent this Log.
	Fields []memstore.Entry `json:"fields,omitempty"`

	// The Request and Response raw bodies.
	// If they are escaped (e.g. JSON),
	// A third-party software can read it through:
	// data, _ := strconv.Unquote(log.Request)
	// err := json.Unmarshal([]byte(data), &customStruct)
	Request  string `json:"request"`
	Response string `json:"response"`

	// A copy of the Request's Context when Async is true (safe to use concurrently),
	// otherwise it's the current Context (not safe for concurrent access).
	Ctx *context.Context `json:"-" yaml:"-" toml:"-"`
}

// RequestValuesLine returns a string line which
// combines the path parameters, query and custom fields.
func (l *Log) RequestValuesLine() string {
	return parseRequestValues(l.Code, l.Ctx.Params(), l.Ctx.URLParamsSorted(), l.Fields)
}

func parseRequestValues(code int, pathParams *context.RequestParams, query []memstore.StringEntry, fields memstore.Store) (requestValues string) {
	var buf strings.Builder

	if !context.StatusCodeNotSuccessful(code) {
		// collect path parameters on a successful request-response only.
		pathParams.Visit(func(key, value string) {
			buf.WriteString(key)
			buf.WriteByte('=')
			buf.WriteString(value)
			buf.WriteByte(' ')
		})
	}

	for _, entry := range query {
		buf.WriteString(entry.Key)
		buf.WriteByte('=')
		buf.WriteString(entry.Value)
		buf.WriteByte(' ')
	}

	for _, entry := range fields {
		buf.WriteString(entry.Key)
		buf.WriteByte('=')
		buf.WriteString(fmt.Sprintf("%v", entry.ValueRaw))
		buf.WriteByte(' ')
	}

	if n := buf.Len(); n > 1 {
		requestValues = buf.String()[0 : n-1] // remove last space.
	}

	return
}

// Formatter is responsible to print a Log to the accesslog's writer.
type Formatter interface {
	// Format should print the Log.
	// Returns nil error on handle successfully,
	// otherwise the log will be printed using the default formatter
	// and the error will be printed to the Iris Application's error log level.
	Format(log *Log) error
	// SetWriter should inject the accesslog's direct output,
	// if this "dest" is used then the Formatter
	// should manually control its concurrent use.
	SetOutput(dest io.Writer)
}

var (
	_ Formatter = (*JSON)(nil)
	_ Formatter = (*Template)(nil)
)

// JSON is a Formatter type for JSON logs.
type JSON struct {
	Prefix, Indent string
	EscapeHTML     bool

	enc *json.Encoder
	mu  sync.Mutex
}

// SetOutput creates the json encoder writes to the "dest".
// It's called automatically by the middleware when this Formatter is used.
func (f *JSON) SetOutput(dest io.Writer) {
	if dest == nil {
		return
	}

	// All logs share the same accesslog's writer and it cannot change during serve-time.
	enc := json.NewEncoder(dest)
	enc.SetEscapeHTML(f.EscapeHTML)
	enc.SetIndent(f.Prefix, f.Indent)
	f.enc = enc
}

// Format prints the logs in JSON format.
// Writes to the destination directly,
// locks on each Format call.
func (f *JSON) Format(log *Log) error {
	f.mu.Lock()
	err := f.enc.Encode(log)
	f.mu.Unlock()

	return err
}

// Template is a Formatter.
// It's used to print the Log in a text/template way.
// The caller has full control over the printable result;
// certain fields can be ignored, change the display order and e.t.c.
type Template struct {
	// Custom template source.
	// Use this or `Tmpl/TmplName` fields.
	Text string
	// Custom template to use, overrides the `Text` field if not nil.
	Tmpl *template.Template
	// If not empty then this named template/block
	// is response to hold the log result.
	TmplName string

	dest io.Writer
	mu   sync.Mutex
}

// SetOutput creates the default template if missing
// when this formatter is registered.
func (f *Template) SetOutput(dest io.Writer) {
	if f.Tmpl == nil {
		text := f.Text
		if f.Text == "" {
			text = defaultTmplText
		}

		f.Tmpl = template.Must(template.New("").Parse(text))
	}

	f.dest = dest
}

const defaultTmplText = "{{.Now.Format .TimeFormat}}|{{.Latency}}|{{.Method}}|{{.Path}}|{{.RequestValuesLine}}|{{.Code}}|{{.Request}}|{{.Response}}|\n"

// Format prints the logs in text/template format.
func (f *Template) Format(log *Log) error {
	var err error

	// A template may be executed safely in parallel, although if parallel
	// executions share a Writer the output may be interleaved.
	f.mu.Lock()
	if f.TmplName != "" {
		err = f.Tmpl.ExecuteTemplate(f.dest, f.TmplName, log)
	} else {
		err = f.Tmpl.Execute(f.dest, log)
	}
	f.mu.Unlock()

	return err
}