package scanner

import (
	"bytes"
	"errors"
	"strconv"
	"strings"

	"github.com/pingcap/log"
	"go.uber.org/zap"
)

var (
	ErrNotFoundParser = errors.New("not suitable log parser")
	ErrLogIncomplete  = errors.New("incomplete log")
	ErrNeedSkipLog    = errors.New("invaild log need to be skip")
)

// Each log package has its own style of log printing
// the hub is used  to try to find a sutiable log package based on the log content
var hub = make(map[string]LogParser)

func RegisterLogParser(name string, l LogParser) {
	if _, exist := hub[name]; exist {
		log.Fatal("log parser already exists in hub", zap.String("kind", name))
	}

	hub[name] = l
}

func init() {
	RegisterLogParser("zap", newZapLogParser())
}

// LogParser defines a log parsing interface,
// which is used to parse logs generated by different log packages
type LogParser interface {
	Parse(content []byte) (*Log, error)
	IsSuitable(content []byte) bool
}

// https://github.com/uber-go/zap
type zapLogParser struct {
}

func newZapLogParser() LogParser {
	return &zapLogParser{}
}

func (z *zapLogParser) IsSuitable(content []byte) bool {
	_, err := z.Parse(content)
	if err != nil {
		log.Error("zap is not suitable parser", zap.String("log", string(content)), zap.Error(err))
	}

	return err == nil
}

func (z *zapLogParser) Parse(content []byte) (*Log, error) {
	zapLog := &ZapLog{Log: &Log{}}
	if err := zapLog.Extract(content); err != nil {
		return nil, err
	}
	return zapLog.Log, nil
}

var constSpaceLsbrck = []byte(" [")

type ZapLog struct {
	Rest []byte
	*Log
}

func isVaildLogEvel(level string) bool {
	levelToLow := strings.ToLower(level)
	return levelToLow == "error" || levelToLow == "info" || levelToLow == "warn" || levelToLow == "fatal" || levelToLow == "debug"
}

func (z *ZapLog) Extract(line []byte) error {
	z.Rest = line
	var pos int

	// Checks if the rest starts with '[' and pass it
	if len(z.Rest) >= 1 && z.Rest[0] == '[' {
		z.Rest = z.Rest[1:]
	} else {
		return ErrNeedSkipLog
	}

	// Take until ']' as Time(string)
	pos = bytes.IndexByte(z.Rest, ']')
	if pos >= 0 {
		z.Time = string(z.Rest[:pos])
		z.Rest = z.Rest[pos+1:]
	} else {
		return ErrLogIncomplete
	}

	// Checks if the rest starts with `" ["` and pass it
	if bytes.HasPrefix(z.Rest, constSpaceLsbrck) {
		z.Rest = z.Rest[len(constSpaceLsbrck):]
	} else {
		return ErrNeedSkipLog
	}

	// Take until ']' as Level(string)
	pos = bytes.IndexByte(z.Rest, ']')
	if pos >= 0 {
		z.Level = string(z.Rest[:pos])
		if !isVaildLogEvel(z.Level) {
			return ErrNeedSkipLog
		}
		z.Rest = z.Rest[pos+1:]
	} else {
		return ErrLogIncomplete
	}

	// Checks if the rest starts with `" ["` and pass it
	if bytes.HasPrefix(z.Rest, constSpaceLsbrck) {
		z.Rest = z.Rest[len(constSpaceLsbrck):]
	} else {
		return ErrNeedSkipLog
	}

	// Take until ']' as Position(string)
	pos = bytes.IndexByte(z.Rest, ']')
	if pos >= 0 {
		z.File = string(z.Rest[:pos])
		position := strings.Split(z.File, ":")
		if len(position) >= 2 {
			line, err := strconv.Atoi(position[1])
			if err == nil {
				z.Line = int32(line)
				z.File = position[0]
			}
		}
		z.Rest = z.Rest[pos+1:]
	} else {
		return ErrLogIncomplete
	}

	// Checks if the rest starts with `" ["` and pass it
	if bytes.HasPrefix(z.Rest, constSpaceLsbrck) {
		z.Rest = z.Rest[len(constSpaceLsbrck):]
	} else {
		return ErrNeedSkipLog
	}

	// Take until ']' as Msg(string)
	pos = bytes.IndexByte(z.Rest, ']')
	if pos >= 0 {
		z.Msg = string(z.Rest[:pos])
		z.Rest = z.Rest[pos+1:]
	} else {
		return ErrLogIncomplete
	}

	return nil
}
