package errors

import (
	"fmt"
	"net/http"
	"path"
	"strconv"
	"strings"
)

//CustomError implements the following interfaces:
//	error
//	github.com/pkg/errors: Cause
type CustomError struct {
	code    int
	message string
	caller  Caller
	cause   error
}

//implement interface error:
func (err CustomError) Error() string {
	return err.Formatted(FormattingOptions{Causes: true})
}

//implement github.com/pkg/errors: Cause
func (err CustomError) Cause() error {
	return err.cause
}

func HTTPCode(err error) int {
	if errWithCode, ok := err.(ErrorWithCause); ok {
		return errWithCode.Code()
	}
	return 0
}

func (err CustomError) Code() int {
	//find http error code - returning the smallest code in the stack of causes (excluding code==0)
	code := err.code
	if err.cause != nil {
		if causeWithCode, ok := err.cause.(ErrorWithCause); ok {
			causeCode := causeWithCode.Code()
			if code == 0 || (causeCode != 0 && causeCode < code) {
				code = causeCode
			}
		}
	}
	return code
}

func (err CustomError) Description() Description {
	info := err.caller.Info()
	desc := &Description{
		Message: err.message,
		Source:  &info,
	}
	if err.cause != nil {
		causeWithStack, ok := err.cause.(*CustomError)
		if !ok {
			//external cause without our stack
			//if github.com/pkg/errors, we can still get caller reference
			desc.Cause = pkgDescription(0, err.cause)
		} else {
			//cause has our stack
			causeDesription := causeWithStack.Description()
			desc.Cause = &causeDesription
		}
	}
	return *desc
}

func (err CustomError) Format(s fmt.State, v rune) {
	s.Write([]byte(
		err.Formatted(
			FormattingOptions{
				Causes:   (v == 'v' || v == 'c'), //include causes for %c and %v, s is only this error
				NewLines: v == 'v',               //use newlines only on v, c is more compact on single line
				Source:   s.Flag('+'),            //include source references when %+v or %+c
			},
		),
	))
}

type FormattingOptions struct {
	Causes   bool
	NewLines bool
	Source   bool
}

func (err CustomError) Formatted(opts FormattingOptions) string {
	//start with this error
	thisError := ""
	if opts.Source {
		thisError += fmt.Sprintf("%s/%s(%d): %s() ",
			err.caller.Package(),
			path.Base(err.caller.File()),
			err.caller.Line(),
			err.caller.Function(),
		)
	}
	thisError += err.message
	if err.code != 0 {
		thisError += fmt.Sprintf(" HTTP(%d:%s)", err.code, http.StatusText(err.code))
	}
	if !opts.Causes {
		return thisError
	}

	if err.cause == nil {
		return thisError
	}

	sep := ", because"
	if opts.NewLines {
		sep += "\n\t"
	} else {
		sep += " "
	}

	if causeWithStack, ok := err.cause.(*CustomError); ok {
		return thisError + sep + causeWithStack.Formatted(opts)
	}

	//this level does not have our own stack, but we can detect github.com/pkg/errors stack, then:
	//note: do not use fmt.Sprintf("%+v", err.cause) because it will repeat the stack if multiple were captured
	//note: do not use err.cause.Error() because it does not include any stack
	//instead, get first layer that implements it and log it
	return thisError + pkgStack(err.cause, opts)
}

func pkgStack(err error, opts FormattingOptions) string {
	s := ""
	e := err
	for e != nil {
		if errWithStackTracer, ok := e.(stackTracer); ok {
			st := errWithStackTracer.StackTrace()
			for _, f := range st {
				//source := fmt.Sprintf("%n %s %d", f, f, f) - this shows only package name, not fully qualified package name :-(
				sources := strings.SplitN(fmt.Sprintf("%+s(%d)\n%n", f, f, f), "\n", 3)
				//package            <-- sources[0]
				//full filename:line <-- source[1]
				//function name      <-- source[2]

				//skip runtime packages
				if strings.HasPrefix(sources[0], "runtime") {
					break
				}
				if opts.NewLines {
					s += ", because \n\t"
				} else {
					s += ", because "
				}
				if opts.Source {
					s += sources[0] + "/" + path.Base(sources[1]) + ": " + sources[2] + "(): "
				}
				s += fmt.Sprintf("%s", e)
			}
			return s
		}
		errWithCause, ok := e.(ErrorWithCause)
		if !ok {
			break
		}
		e = errWithCause.Cause()
	}
	return s
}

func pkgDescription(level int, err error) *Description {
	desc := &Description{
		Message: fmt.Sprintf("%s", err),
		Source:  nil,
		Cause:   nil,
	}

	//recursively fill causes first
	if errWithCause, ok := err.(ErrorWithCause); ok {
		causeErr := errWithCause.Cause()
		if causeErr != nil {
			desc.Cause = pkgDescription(level+1, causeErr)
		}
	}

	if errWithStackTracer, ok := err.(stackTracer); ok {
		tempDesc := desc
		st := errWithStackTracer.StackTrace()
		for _, f := range st {
			if tempDesc == nil {
				break //stop if no more causes populated
			}

			//source := fmt.Sprintf("%n %s %d", f, f, f) - this shows only package name, not fully qualified package name :-(
			sources := strings.SplitN(fmt.Sprintf("%+s\n%d\n%n", f, f, f), "\n", 4)
			//package            <-- sources[0]
			//full filename      <-- source[1]
			//line               <-- source[2]
			//function name      <-- source[3]

			//stop if entering runtime part of the stack
			if strings.HasPrefix(sources[0], "runtime") {
				break
			}
			tempDesc.Source = &CallerInfo{
				Package: sources[0],
				File:    path.Base(sources[1]),
				//Line:     sources[2],
				Function: sources[3],
			}
			if i64, err := strconv.ParseInt(sources[2], 10, 64); err == nil {
				tempDesc.Source.Line = int(i64)
			}
			tempDesc = tempDesc.Cause
		} //for each stack level
	} //if has stack
	return desc
}