// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package errorreporting is a Google Stackdriver Error Reporting library.
//
// This package is still experimental and subject to change.
//
// See https://cloud.google.com/error-reporting/ for more information.
package errorreporting // import "cloud.google.com/go/errorreporting"

import (
	"bytes"
	"fmt"
	"log"
	"net/http"
	"runtime"
	"time"

	api "cloud.google.com/go/errorreporting/apiv1beta1"
	"cloud.google.com/go/internal/version"
	"github.com/golang/protobuf/ptypes"
	gax "github.com/googleapis/gax-go"
	"golang.org/x/net/context"
	"google.golang.org/api/option"
	"google.golang.org/api/support/bundler"
	erpb "google.golang.org/genproto/googleapis/devtools/clouderrorreporting/v1beta1"
)

const (
	userAgent = `gcloud-golang-errorreporting/20160701`
)

// Config is additional configuration for Client.
type Config struct {
	// ServiceName identifies the running program and is included in the error reports.
	// Optional.
	ServiceName string

	// ServiceVersion identifies the version of the running program and is
	// included in the error reports.
	// Optional.
	ServiceVersion string

	// OnError is the function to call if any background
	// tasks errored. By default, errors are logged.
	OnError func(err error)
}

// Entry holds information about the reported error.
type Entry struct {
	Error error
	Req   *http.Request // if error is associated with a request.
	Stack []byte        // if user does not provide a stack trace, runtime.Stack will be called
}

// Client represents a Google Cloud Error Reporting client.
type Client struct {
	projectID      string
	apiClient      client
	serviceContext erpb.ServiceContext
	bundler        *bundler.Bundler

	onErrorFn func(err error)
}

var newClient = func(ctx context.Context, opts ...option.ClientOption) (client, error) {
	client, err := api.NewReportErrorsClient(ctx, opts...)
	if err != nil {
		return nil, err
	}
	client.SetGoogleClientInfo("gccl", version.Repo)
	return client, nil
}

// NewClient returns a new error reporting client. Generally you will want
// to create a client on program initialization and use it through the lifetime
// of the process.
func NewClient(ctx context.Context, projectID string, cfg Config, opts ...option.ClientOption) (*Client, error) {
	if cfg.ServiceName == "" {
		cfg.ServiceName = "goapp"
	}
	c, err := newClient(ctx, opts...)
	if err != nil {
		return nil, fmt.Errorf("creating client: %v", err)
	}

	client := &Client{
		apiClient: c,
		projectID: "projects/" + projectID,
		serviceContext: erpb.ServiceContext{
			Service: cfg.ServiceName,
			Version: cfg.ServiceVersion,
		},
	}
	bundler := bundler.NewBundler((*erpb.ReportErrorEventRequest)(nil), func(bundle interface{}) {
		reqs := bundle.([]*erpb.ReportErrorEventRequest)
		for _, req := range reqs {
			_, err = client.apiClient.ReportErrorEvent(ctx, req)
			if err != nil {
				client.onError(fmt.Errorf("failed to upload: %v", err))
			}
		}
	})
	// TODO(jbd): Optimize bundler limits.
	bundler.DelayThreshold = 2 * time.Second
	bundler.BundleCountThreshold = 100
	bundler.BundleByteThreshold = 1000
	bundler.BundleByteLimit = 1000
	bundler.BufferedByteLimit = 10000
	client.bundler = bundler
	return client, nil
}

func (c *Client) onError(err error) {
	if c.onErrorFn != nil {
		c.onErrorFn(err)
		return
	}
	log.Println(err)
}

// Close closes any resources held by the client.
// Close should be called when the client is no longer needed.
// It need not be called at program exit.
func (c *Client) Close() error {
	return c.apiClient.Close()
}

// Report writes an error report. It doesn't block. Errors in
// writing the error report can be handled via Client.OnError.
func (c *Client) Report(e Entry) {
	var stack string
	if e.Stack != nil {
		stack = string(e.Stack)
	}
	req := c.makeReportErrorEventRequest(e.Req, e.Error.Error(), stack)
	c.bundler.Add(req, 1)
}

// ReportSync writes an error report. It blocks until the entry is written.
func (c *Client) ReportSync(ctx context.Context, e Entry) error {
	var stack string
	if e.Stack != nil {
		stack = string(e.Stack)
	}
	req := c.makeReportErrorEventRequest(e.Req, e.Error.Error(), stack)
	_, err := c.apiClient.ReportErrorEvent(ctx, req)
	return err
}

// Flush blocks until all currently buffered error reports are sent.
//
// If any errors occurred since the last call to Flush, or the
// creation of the client if this is the first call, then Flush report the
// error via the (*Client).OnError handler.
func (c *Client) Flush() {
	c.bundler.Flush()
}

func (c *Client) makeReportErrorEventRequest(r *http.Request, msg string, stack string) *erpb.ReportErrorEventRequest {
	if stack == "" {
		// limit the stack trace to 16k.
		var buf [16 * 1024]byte
		stack = chopStack(buf[0:runtime.Stack(buf[:], false)])
	}
	message := msg + "\n" + stack

	var errorContext *erpb.ErrorContext
	if r != nil {
		errorContext = &erpb.ErrorContext{
			HttpRequest: &erpb.HttpRequestContext{
				Method:    r.Method,
				Url:       r.Host + r.RequestURI,
				UserAgent: r.UserAgent(),
				Referrer:  r.Referer(),
				RemoteIp:  r.RemoteAddr,
			},
		}
	}
	return &erpb.ReportErrorEventRequest{
		ProjectName: c.projectID,
		Event: &erpb.ReportedErrorEvent{
			EventTime:      ptypes.TimestampNow(),
			ServiceContext: &c.serviceContext,
			Message:        message,
			Context:        errorContext,
		},
	}
}

// chopStack trims a stack trace so that the function which panics or calls
// Report is first.
func chopStack(s []byte) string {
	f := []byte("cloud.google.com/go/errorreporting.(*Client).Report")

	lfFirst := bytes.IndexByte(s, '\n')
	if lfFirst == -1 {
		return string(s)
	}
	stack := s[lfFirst:]
	panicLine := bytes.Index(stack, f)
	if panicLine == -1 {
		return string(s)
	}
	stack = stack[panicLine+1:]
	for i := 0; i < 2; i++ {
		nextLine := bytes.IndexByte(stack, '\n')
		if nextLine == -1 {
			return string(s)
		}
		stack = stack[nextLine+1:]
	}
	return string(s[:lfFirst+1]) + string(stack)
}

type client interface {
	ReportErrorEvent(ctx context.Context, req *erpb.ReportErrorEventRequest, opts ...gax.CallOption) (*erpb.ReportErrorEventResponse, error)
	Close() error
}