package rollbar

import (
	"bytes"
	"encoding/json"
	"fmt"
	"hash/adler32"
	"net/http"
	"net/url"
	"os"
	"reflect"
	"regexp"
	"runtime"
	"strings"
	"sync"
	"time"
)

const (
	// NAME is the name of this notifier library as reported to the Rollbar API.
	NAME = "go-rollbar"

	// VERSION is the version number of this notifier library as reported to the
	// Rollbar API.
	VERSION = "0.4.0"

	// CRIT is the critical Rollbar severity level as reported to the Rollbar
	// API.
	CRIT = "critical"

	// ERR is the error Rollbar severity level as reported to the Rollbar API.
	ERR = "error"

	// WARN is the warning Rollbar severity level as reported to the Rollbar API.
	WARN = "warning"

	// INFO is the info Rollbar severity level as reported to the Rollbar API.
	INFO = "info"

	// DEBUG is the debug Rollbar severity level as reported to the Rollbar API.
	DEBUG = "debug"

	// FILTERED is the text that replaces all sensitive values in items sent to
	// the Rollbar API.
	FILTERED = "[FILTERED]"
)

var (
	// Token is the Rollbar access token under which all items will be reported.
	// If Token is blank, no errors will be reported to Rollbar.
	Token = ""

	// Environment is the environment under which all items will be reported.
	Environment = "development"

	// Platform is the platform reported for all Rollbar items. The default is
	// the running operating system (darwin, freebsd, linux, etc.) but it can
	// also be application specific (client, heroku, etc.).
	Platform = runtime.GOOS

	// Endpoint is the URL destination for all Rollbar item POST requests.
	Endpoint = "https://api.rollbar.com/api/1/item/"

	// Buffer is the maximum number of errors that will be queued for sending.
	// When the buffer is full, new errors are dropped on the floor until the API
	// can catch up.
	Buffer = 1000

	// FilterFields is a regular expression that matches field names that should
	// not be sent to Rollbar. Values for these fields are replaced with
	// "[FILTERED]".
	FilterFields = regexp.MustCompile("password|secret|token")

	// ErrorWriter is the destination for errors encountered while POSTing items
	// to Rollbar. By default, this is stderr. This can be nil.
	ErrorWriter = os.Stderr

	// CodeVersion is the optional code version reported to the Rollbar API for
	// all items.
	CodeVersion = ""

	bodyChannel chan map[string]interface{}
	waitGroup   sync.WaitGroup
	postErrors  chan error
	nilErrTitle = "<nil>"
)

// Field is a custom data field used to report arbitrary data to the Rollbar
// API.
type Field struct {
	Name string
	Data interface{}
}

// -- Setup

func init() {
	bodyChannel = make(chan map[string]interface{}, Buffer)
	postErrors = make(chan error, Buffer)

	go func() {
		var err error
		for body := range bodyChannel {
			err = post(body)
			if err != nil {
				if len(postErrors) == cap(postErrors) {
					<-postErrors
				}
				postErrors <- err
			}
			waitGroup.Done()
		}
		close(postErrors)
	}()
}

// -- Error reporting

func Errorf(level string, format string, args ...interface{}) {
	ErrorWithStackSkip(level, fmt.Errorf(format, args), 1)
}

// Error asynchronously sends an error to Rollbar with the given severity
// level. You can pass, optionally, custom Fields to be passed on to Rollbar.
func Error(level string, err error, fields ...*Field) {
	ErrorWithStackSkip(level, err, 1, fields...)
}

// ErrorWithStackSkip asynchronously sends an error to Rollbar with the given
// severity level and a given number of stack trace frames skipped. You can
// pass, optionally, custom Fields to be passed on to Rollbar.
func ErrorWithStackSkip(level string, err error, skip int, fields ...*Field) {
	stack := BuildStack(2 + skip)
	ErrorWithStack(level, err, stack, fields...)
}

// ErrorWithStack asynchronously sends and error to Rollbar with the given
// stacktrace and (optionally) custom Fields to be passed on to Rollbar.
func ErrorWithStack(level string, err error, stack Stack, fields ...*Field) {
	buildAndPushError(level, err, stack, fields...)
}

// RequestError asynchronously sends an error to Rollbar with the given
// severity level and request-specific information. You can pass, optionally,
// custom Fields to be passed on to Rollbar.
func RequestError(level string, r *http.Request, err error, fields ...*Field) {
	RequestErrorWithStackSkip(level, r, err, 1, fields...)
}

// RequestErrorWithStackSkip asynchronously sends an error to Rollbar with the
// given severity level and a given number of stack trace frames skipped, in
// addition to extra request-specific information. You can pass, optionally,
// custom Fields to be passed on to Rollbar.
func RequestErrorWithStackSkip(level string, r *http.Request, err error, skip int, fields ...*Field) {
	stack := BuildStack(2 + skip)
	RequestErrorWithStack(level, r, err, stack, fields...)
}

// RequestErrorWithStack asynchronously sends an error to Rollbar with the
// given severity level, request-specific information provided by the given
// http.Request, and a custom Stack. You You can pass, optionally, custom
// Fields to be passed on to Rollbar.
func RequestErrorWithStack(level string, r *http.Request, err error, stack Stack, fields ...*Field) {
	buildAndPushError(level, err, stack, append(fields, &Field{Name: "request", Data: errorRequest(r)})...)
}

func buildError(level string, err error, stack Stack, fields ...*Field) map[string]interface{} {
	title := nilErrTitle
	if err != nil {
		title = err.Error()
	}

	body := buildBody(level, title)
	data := body["data"].(map[string]interface{})
	errBody, fingerprint := errorBody(err, stack)
	data["body"] = errBody
	data["fingerprint"] = fingerprint

	for _, field := range fields {
		data[field.Name] = field.Data
	}

	return body
}

func buildAndPushError(level string, err error, stack Stack, fields ...*Field) {
	push(buildError(level, err, stack, fields...))
}

// -- Message reporting

// Message asynchronously sends a message to Rollbar with the given severity
// level.
func Message(level string, msg string) {
	body := buildBody(level, msg)
	data := body["data"].(map[string]interface{})
	data["body"] = messageBody(msg)

	push(body)
}

// -- Misc.

// PostErrors returns a channel that receives all errors encountered while
// POSTing items to the Rollbar API.
func PostErrors() <-chan error {
	return postErrors
}

// Wait will block until the queue of errors / messages is empty. This allows
// you to ensure that errors / messages are sent to Rollbar before exiting an
// application.
func Wait() {
	waitGroup.Wait()
}

// Build the main JSON structure that will be sent to Rollbar with the
// appropriate metadata.
func buildBody(level, title string) map[string]interface{} {
	timestamp := time.Now().Unix()
	hostname, _ := os.Hostname()

	data := map[string]interface{}{
		"environment": Environment,
		"title":       title,
		"level":       level,
		"timestamp":   timestamp,
		"platform":    Platform,
		"language":    "go",
		"server": map[string]interface{}{
			"host": hostname,
		},
		"notifier": map[string]interface{}{
			"name":    NAME,
			"version": VERSION,
		},
	}
	if CodeVersion != "" {
		data["code_version"] = CodeVersion
	}

	return map[string]interface{}{
		"access_token": Token,
		"data":         data,
	}
}

// errorBody generates a Rollbar error body with a given stack trace.
func errorBody(err error, stack Stack) (map[string]interface{}, string) {
	message := nilErrTitle
	if err != nil {
		message = err.Error()
	}

	fingerprint := stack.Fingerprint()
	errBody := map[string]interface{}{
		"trace": map[string]interface{}{
			"frames": stack,
			"exception": map[string]interface{}{
				"class":   errorClass(err),
				"message": message,
			},
		},
	}
	return errBody, fingerprint
}

// errorRequest extracts details from a Request in a format that Rollbar
// accepts.
func errorRequest(r *http.Request) map[string]interface{} {
	cleanQuery := filterParams(r.URL.Query())

	return map[string]interface{}{
		"url":     r.URL.String(),
		"method":  r.Method,
		"headers": flattenValues(r.Header),

		// GET params
		"query_string": url.Values(cleanQuery).Encode(),
		"GET":          flattenValues(cleanQuery),

		// POST / PUT params
		"POST":    flattenValues(filterParams(r.Form)),
		"user_ip": r.RemoteAddr,
	}
}

// filterParams filters sensitive information like passwords from being sent to
// Rollbar.
func filterParams(values map[string][]string) map[string][]string {
	for key := range values {
		if FilterFields.Match([]byte(key)) {
			values[key] = []string{FILTERED}
		}
	}

	return values
}

func flattenValues(values map[string][]string) map[string]interface{} {
	result := make(map[string]interface{})

	for k, v := range values {
		if len(v) == 1 {
			result[k] = v[0]
		} else {
			result[k] = v
		}
	}

	return result
}

// Build a message inner-body for the given message string.
func messageBody(s string) map[string]interface{} {
	return map[string]interface{}{
		"message": map[string]interface{}{
			"body": s,
		},
	}
}

func errorClass(err error) string {
	if err == nil {
		return nilErrTitle
	}

	class := reflect.TypeOf(err).String()
	if class == "" {
		return "panic"
	} else if class == "*errors.errorString" {
		checksum := adler32.Checksum([]byte(err.Error()))
		return fmt.Sprintf("{%x}", checksum)
	} else {
		return strings.TrimPrefix(class, "*")
	}
}

// -- POST handling

// Queue the given JSON body to be POSTed to Rollbar.
func push(body map[string]interface{}) {
	if len(bodyChannel) < Buffer {
		waitGroup.Add(1)
		bodyChannel <- body
	} else {
		stderr("buffer full, dropping error on the floor")
	}
}

// POST the given JSON body to Rollbar synchronously.
func post(body map[string]interface{}) error {
	if len(Token) == 0 {
		stderr("empty token")
		return nil
	}

	jsonBody, err := json.Marshal(body)
	if err != nil {
		stderr("failed to encode payload: %s", err.Error())
		return err
	}

	resp, err := http.Post(Endpoint, "application/json", bytes.NewReader(jsonBody))
	if err != nil {
		stderr("POST failed: %s", err.Error())
		return err
	}

	if resp.StatusCode != 200 {
		stderr("received response: %s", resp.Status)
		return ErrHTTPError(resp.StatusCode)
	}

	if resp != nil {
		resp.Body.Close()
	}

	return nil
}

// -- stderr
func stderr(format string, args ...interface{}) {
	if ErrorWriter != nil {
		format = "Rollbar error: " + format + "\n"
		fmt.Fprintf(ErrorWriter, format, args...)
	}
}
