package api

import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"plugin"
	"sync"
	"time"

	"github.com/google/martian/har"
)

type Protocol struct {
	Name            string   `json:"name"`
	LongName        string   `json:"longName"`
	Abbreviation    string   `json:"abbr"`
	Macro           string   `json:"macro"`
	Version         string   `json:"version"`
	BackgroundColor string   `json:"backgroundColor"`
	ForegroundColor string   `json:"foregroundColor"`
	FontSize        int8     `json:"fontSize"`
	ReferenceLink   string   `json:"referenceLink"`
	Ports           []string `json:"ports"`
	Priority        uint8    `json:"priority"`
}

type TCP struct {
	IP   string `json:"ip"`
	Port string `json:"port"`
	Name string `json:"name"`
}

type Extension struct {
	Protocol   *Protocol
	Path       string
	Plug       *plugin.Plugin
	Dissector  Dissector
	MatcherMap *sync.Map
}

type ConnectionInfo struct {
	ClientIP   string
	ClientPort string
	ServerIP   string
	ServerPort string
	IsOutgoing bool
}

type TcpID struct {
	SrcIP   string
	DstIP   string
	SrcPort string
	DstPort string
	Ident   string
}

type CounterPair struct {
	Request  uint
	Response uint
}

type GenericMessage struct {
	IsRequest   bool        `json:"isRequest"`
	CaptureTime time.Time   `json:"captureTime"`
	Payload     interface{} `json:"payload"`
}

type RequestResponsePair struct {
	Request  GenericMessage `json:"request"`
	Response GenericMessage `json:"response"`
}

// `Protocol` is modified in the later stages of data propagation. Therefore it's not a pointer.
type OutputChannelItem struct {
	Protocol       Protocol
	Timestamp      int64
	ConnectionInfo *ConnectionInfo
	Pair           *RequestResponsePair
	Summary        *BaseEntry
}

type SuperTimer struct {
	CaptureTime time.Time
}

type SuperIdentifier struct {
	Protocol       *Protocol
	IsClosedOthers bool
}

type Dissector interface {
	Register(*Extension)
	Ping()
	Dissect(b *bufio.Reader, isClient bool, tcpID *TcpID, counterPair *CounterPair, superTimer *SuperTimer, superIdentifier *SuperIdentifier, emitter Emitter, options *TrafficFilteringOptions) error
	Analyze(item *OutputChannelItem, resolvedSource string, resolvedDestination string) *Entry
	Represent(request map[string]interface{}, response map[string]interface{}) (object []byte, bodySize int64, err error)
	Macros() map[string]string
}

type Emitting struct {
	AppStats      *AppStats
	OutputChannel chan *OutputChannelItem
}

type Emitter interface {
	Emit(item *OutputChannelItem)
}

func (e *Emitting) Emit(item *OutputChannelItem) {
	e.OutputChannel <- item
	e.AppStats.IncMatchedPairs()
}

type Entry struct {
	Id                     uint                   `json:"id"`
	Protocol               Protocol               `json:"proto"`
	Source                 *TCP                   `json:"src"`
	Destination            *TCP                   `json:"dst"`
	Outgoing               bool                   `json:"outgoing"`
	Timestamp              int64                  `json:"timestamp"`
	StartTime              time.Time              `json:"startTime"`
	Request                map[string]interface{} `json:"request"`
	Response               map[string]interface{} `json:"response"`
	Summary                string                 `json:"summary"`
	Method                 string                 `json:"method"`
	Status                 int                    `json:"status"`
	ElapsedTime            int64                  `json:"elapsedTime"`
	Path                   string                 `json:"path"`
	IsOutgoing             bool                   `json:"isOutgoing,omitempty"`
	Rules                  ApplicableRules        `json:"rules,omitempty"`
	ContractStatus         ContractStatus         `json:"contractStatus,omitempty"`
	ContractRequestReason  string                 `json:"contractRequestReason,omitempty"`
	ContractResponseReason string                 `json:"contractResponseReason,omitempty"`
	ContractContent        string                 `json:"contractContent,omitempty"`
	HTTPPair               string                 `json:"httpPair,omitempty"`
}

type EntryWrapper struct {
	Protocol       Protocol                 `json:"protocol"`
	Representation string                   `json:"representation"`
	BodySize       int64                    `json:"bodySize"`
	Data           *Entry                   `json:"data"`
	Rules          []map[string]interface{} `json:"rulesMatched,omitempty"`
	IsRulesEnabled bool                     `json:"isRulesEnabled"`
}

type BaseEntry struct {
	Id             uint            `json:"id"`
	Protocol       Protocol        `json:"proto,omitempty"`
	Url            string          `json:"url,omitempty"`
	Path           string          `json:"path,omitempty"`
	Summary        string          `json:"summary,omitempty"`
	StatusCode     int             `json:"status"`
	Method         string          `json:"method,omitempty"`
	Timestamp      int64           `json:"timestamp,omitempty"`
	Source         *TCP            `json:"src"`
	Destination    *TCP            `json:"dst"`
	IsOutgoing     bool            `json:"isOutgoing,omitempty"`
	Latency        int64           `json:"latency"`
	Rules          ApplicableRules `json:"rules,omitempty"`
	ContractStatus ContractStatus  `json:"contractStatus"`
}

type ApplicableRules struct {
	Latency       int64 `json:"latency,omitempty"`
	Status        bool  `json:"status,omitempty"`
	NumberOfRules int   `json:"numberOfRules,omitempty"`
}

type ContractStatus int

type Contract struct {
	Status         ContractStatus `json:"status"`
	RequestReason  string         `json:"requestReason"`
	ResponseReason string         `json:"responseReason"`
	Content        string         `json:"content"`
}

func Summarize(entry *Entry) *BaseEntry {
	return &BaseEntry{
		Id:             entry.Id,
		Protocol:       entry.Protocol,
		Path:           entry.Path,
		Summary:        entry.Summary,
		StatusCode:     entry.Status,
		Method:         entry.Method,
		Timestamp:      entry.Timestamp,
		Source:         entry.Source,
		Destination:    entry.Destination,
		IsOutgoing:     entry.IsOutgoing,
		Latency:        entry.ElapsedTime,
		Rules:          entry.Rules,
		ContractStatus: entry.ContractStatus,
	}
}

type DataUnmarshaler interface {
	UnmarshalData(*Entry) error
}

func (bed *BaseEntry) UnmarshalData(entry *Entry) error {
	bed.Protocol = entry.Protocol
	bed.Id = entry.Id
	bed.Path = entry.Path
	bed.Summary = entry.Summary
	bed.StatusCode = entry.Status
	bed.Method = entry.Method
	bed.Timestamp = entry.Timestamp
	bed.Source = entry.Source
	bed.Destination = entry.Destination
	bed.IsOutgoing = entry.IsOutgoing
	bed.Latency = entry.ElapsedTime
	bed.ContractStatus = entry.ContractStatus
	return nil
}

const (
	TABLE string = "table"
	BODY  string = "body"
)

type SectionData struct {
	Type     string `json:"type"`
	Title    string `json:"title"`
	Data     string `json:"data"`
	Encoding string `json:"encoding,omitempty"`
	MimeType string `json:"mimeType,omitempty"`
	Selector string `json:"selector,omitempty"`
}

type TableData struct {
	Name     string      `json:"name"`
	Value    interface{} `json:"value"`
	Selector string      `json:"selector"`
}

const (
	TypeHttpRequest = iota
	TypeHttpResponse
)

type HTTPPayload struct {
	Type uint8
	Data interface{}
}

type HTTPPayloader interface {
	MarshalJSON() ([]byte, error)
}

type HTTPWrapper struct {
	Method      string               `json:"method"`
	Url         string               `json:"url"`
	Details     interface{}          `json:"details"`
	RawRequest  *HTTPRequestWrapper  `json:"rawRequest"`
	RawResponse *HTTPResponseWrapper `json:"rawResponse"`
}

func (h HTTPPayload) MarshalJSON() ([]byte, error) {
	switch h.Type {
	case TypeHttpRequest:
		harRequest, err := har.NewRequest(h.Data.(*http.Request), true)
		if err != nil {
			return nil, errors.New("Failed converting request to HAR")
		}
		return json.Marshal(&HTTPWrapper{
			Method:     harRequest.Method,
			Details:    harRequest,
			RawRequest: &HTTPRequestWrapper{Request: h.Data.(*http.Request)},
		})
	case TypeHttpResponse:
		harResponse, err := har.NewResponse(h.Data.(*http.Response), true)
		if err != nil {
			return nil, errors.New("Failed converting response to HAR")
		}
		return json.Marshal(&HTTPWrapper{
			Method:      "",
			Url:         "",
			Details:     harResponse,
			RawResponse: &HTTPResponseWrapper{Response: h.Data.(*http.Response)},
		})
	default:
		panic(fmt.Sprintf("HTTP payload cannot be marshaled: %s", h.Type))
	}
}

type HTTPWrapperTricky struct {
	Method      string         `json:"method"`
	Url         string         `json:"url"`
	Details     interface{}    `json:"details"`
	RawRequest  *http.Request  `json:"rawRequest"`
	RawResponse *http.Response `json:"rawResponse"`
}

type HTTPMessage struct {
	IsRequest   bool              `json:"isRequest"`
	CaptureTime time.Time         `json:"captureTime"`
	Payload     HTTPWrapperTricky `json:"payload"`
}

type HTTPRequestResponsePair struct {
	Request  HTTPMessage `json:"request"`
	Response HTTPMessage `json:"response"`
}

type HTTPRequestWrapper struct {
	*http.Request
}

func (r *HTTPRequestWrapper) MarshalJSON() ([]byte, error) {
	body, _ := ioutil.ReadAll(r.Request.Body)
	r.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
	return json.Marshal(&struct {
		Body    string `json:"Body,omitempty"`
		GetBody string `json:"GetBody,omitempty"`
		Cancel  string `json:"Cancel,omitempty"`
		*http.Request
	}{
		Body:    string(body),
		Request: r.Request,
	})
}

type HTTPResponseWrapper struct {
	*http.Response
}

func (r *HTTPResponseWrapper) MarshalJSON() ([]byte, error) {
	body, _ := ioutil.ReadAll(r.Response.Body)
	r.Response.Body = ioutil.NopCloser(bytes.NewBuffer(body))
	return json.Marshal(&struct {
		Body    string `json:"Body,omitempty"`
		GetBody string `json:"GetBody,omitempty"`
		Cancel  string `json:"Cancel,omitempty"`
		*http.Response
	}{
		Body:     string(body),
		Response: r.Response,
	})
}
