package builder

import (
	"net/url"
	"strings"

	"openreplay/backend/pkg/intervals"
	. "openreplay/backend/pkg/messages"
)

func getURLExtention(URL string) string {
	u, err := url.Parse(URL)
	if err != nil {
		return ""
	}
	i := strings.LastIndex(u.Path, ".")
	return u.Path[i+1:]
}

func getResourceType(initiator string, URL string) string {
	switch initiator {
	case "xmlhttprequest", "fetch":
		return "fetch"
	case "img":
		return "img"
	default:
		switch getURLExtention(URL) {
		case "css":
			return "stylesheet"
		case "js":
			return "script"
		case "png", "gif", "jpg", "jpeg", "svg":
			return "img"
		case "mp4", "mkv", "ogg", "webm", "avi", "mp3":
			return "media"
		default:
			return "other"
		}
	}
}

type builder struct {
	readyMsgs                         []Message
	timestamp                         uint64
	peBuilder 												*pageEventBuilder
	ptaBuilder       									*performanceTrackAggrBuilder
	ieBuilder													*inputEventBuilder
	ciFinder													*cpuIssueFinder
	miFinder													*memoryIssueFinder
	ddDetector												*domDropDetector
	crDetector												*clickRageDetector
	dcDetector												*deadClickDetector
	integrationsWaiting 	   					bool


	sid uint64
}

func NewBuilder() *builder {
	return &builder{
		peBuilder: &pageEventBuilder{},
		ptaBuilder: &performanceTrackAggrBuilder{},
		ieBuilder: NewInputEventBuilder(),
		ciFinder: &cpuIssueFinder{},
		miFinder: &memoryIssueFinder{},
		ddDetector: &domDropDetector{},
		crDetector: &clickRageDetector{},
		dcDetector: &deadClickDetector{},
		integrationsWaiting: true,
	}
}

func (b *builder) appendReadyMessage(msg Message) { // interface is never nil even if it holds nil value
	b.readyMsgs = append(b.readyMsgs, msg)
}

func (b *builder) iterateReadyMessage(iter func(msg Message)) {
	for _, readyMsg := range b.readyMsgs {
		iter(readyMsg)
	}
	b.readyMsgs = nil
}

func (b *builder) buildSessionEnd() {
	if b.timestamp == 0 {
		return
	}
	sessionEnd := &SessionEnd{
		Timestamp: b.timestamp, // + delay?
	}
	b.appendReadyMessage(sessionEnd)
}

func (b *builder) buildPageEvent() {
	if msg := b.peBuilder.Build(); msg != nil {
		b.appendReadyMessage(msg)
	}
}
func (b *builder) buildPerformanceTrackAggr() {
	if msg := b.ptaBuilder.Build(); msg != nil {
		b.appendReadyMessage(msg)
	}
}
func (b *builder) buildInputEvent() {
	if msg := b.ieBuilder.Build(); msg != nil {
		b.appendReadyMessage(msg)
	}
}

func (b *builder) handleMessage(message Message, messageID uint64) {
	timestamp := uint64(message.Meta().Timestamp)
	if b.timestamp <= timestamp { // unnecessary. TODO: test and remove
		b.timestamp = timestamp
	}
	// Before  the first timestamp.
	switch msg := message.(type) {
	case *SessionStart,
			*Metadata,
			*UserID,
	  	*UserAnonymousID:
		b.appendReadyMessage(msg)
	case *RawErrorEvent:
		b.appendReadyMessage(&ErrorEvent{
			MessageID: messageID,
			Timestamp: msg.Timestamp,
			Source:    msg.Source,
			Name:      msg.Name,
			Message:   msg.Message,
			Payload:   msg.Payload,
		})
	}
	if b.timestamp == 0 {
		return
	}
	switch msg := message.(type) {
	case *SetPageLocation:
		if msg.NavigationStart == 0 {
			b.appendReadyMessage(&PageEvent{
				URL:       msg.URL,
				Referrer:  msg.Referrer,
				Loaded:    false,
				MessageID: messageID,
				Timestamp: b.timestamp,
			})
		} else {
			b.buildPageEvent()
			b.buildInputEvent()
			b.ieBuilder.ClearLabels()
			b.peBuilder.HandleSetPageLocation(msg, messageID, b.timestamp)
			b.miFinder.HandleSetPageLocation(msg)
			b.ciFinder.HandleSetPageLocation(msg)
		}
	case *PageLoadTiming:
		if rm := b.peBuilder.HandlePageLoadTiming(msg); rm != nil {
			b.appendReadyMessage(rm)
		}
	case *PageRenderTiming:
		if rm := b.peBuilder.HandlePageRenderTiming(msg); rm != nil {
			b.appendReadyMessage(rm)
		}
	case *PerformanceTrack:
		if rm := b.ptaBuilder.HandlePerformanceTrack(msg, b.timestamp); rm != nil {
			b.appendReadyMessage(rm)
		}
		if rm := b.ciFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil {
			b.appendReadyMessage(rm)
		}
		if rm := b.miFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil {
			b.appendReadyMessage(rm)
		}
	case *SetInputTarget:
		if rm := b.ieBuilder.HandleSetInputTarget(msg); rm != nil {
			b.appendReadyMessage(rm)
		}
	case *SetInputValue:
		if rm := b.ieBuilder.HandleSetInputValue(msg, messageID, b.timestamp); rm != nil {
			b.appendReadyMessage(rm)
		}
	case *MouseClick:
		b.buildInputEvent()
		if rm := b.crDetector.HandleMouseClick(msg, messageID, b.timestamp); rm != nil {
			b.appendReadyMessage(rm)
		}
		if msg.Label != "" {
			b.appendReadyMessage(&ClickEvent{
				MessageID:      messageID,
				Label:          msg.Label,
				HesitationTime: msg.HesitationTime,
				Timestamp:      b.timestamp,
				Selector:       msg.Selector,
			})
		}
	case *JSException:
		b.appendReadyMessage(&ErrorEvent{
			MessageID: messageID,
			Timestamp: b.timestamp,
			Source:    "js_exception",
			Name:      msg.Name,
			Message:   msg.Message,
			Payload:   msg.Payload,
		})
	case *ResourceTiming:
		tp := getResourceType(msg.Initiator, msg.URL)
		success := msg.Duration != 0
		b.appendReadyMessage(&ResourceEvent{
			MessageID:       messageID,
			Timestamp:       msg.Timestamp,
			Duration:        msg.Duration,
			TTFB:            msg.TTFB,
			HeaderSize:      msg.HeaderSize,
			EncodedBodySize: msg.EncodedBodySize,
			DecodedBodySize: msg.DecodedBodySize,
			URL:             msg.URL,
			Type:            tp,
			Success:         success,
		})
	  if !success && tp == "fetch" {
		  b.appendReadyMessage(&IssueEvent{
		  	Type: "bad_request",
				MessageID: messageID,
				Timestamp: msg.Timestamp,
				ContextString: msg.URL,
				Context: "",
				Payload: "",
			})
		}
	case *RawCustomEvent:
		b.appendReadyMessage(&CustomEvent{
			MessageID: messageID,
			Timestamp: b.timestamp,
			Name:      msg.Name,
			Payload:   msg.Payload,
		})
	case *CustomIssue:
		b.appendReadyMessage(&IssueEvent{
			Type: "custom",
			Timestamp: b.timestamp,
			MessageID: messageID,
			ContextString: msg.Name,
			Payload: msg.Payload,
		})
	case *Fetch:
		b.appendReadyMessage(&ResourceEvent{
			MessageID: messageID,
			Timestamp: msg.Timestamp,
			Duration:  msg.Duration,
			URL:       msg.URL,
			Type:      "fetch",
			Success:   msg.Status < 300,
			Method:    msg.Method,
			Status:    msg.Status,
		})
	case *StateAction:
		b.appendReadyMessage(&StateActionEvent{
			MessageID: messageID,
			Timestamp: b.timestamp,
			Type:      msg.Type,
		})
	case *GraphQL:
		b.appendReadyMessage(&GraphQLEvent{
			MessageID: messageID,
			Timestamp: b.timestamp,
			Name:      msg.OperationName,
		})
	case *CreateElementNode,
		*CreateTextNode:
		b.ddDetector.HandleNodeCreation()
	case *RemoveNode:
		b.ddDetector.HandleNodeRemoval(b.timestamp)
	case *CreateDocument:
		if rm := b.ddDetector.Build(); rm != nil {
			b.appendReadyMessage(rm)
		}
	}
	if rm := b.dcDetector.HandleMessage(message, messageID, b.timestamp); rm != nil {
		b.appendReadyMessage(rm)
	}
}


func (b *builder) checkTimeouts(ts int64) bool {
	if b.timestamp == 0 { 
		return false // There was no timestamp events yet
	} 

	if b.peBuilder.HasInstance() && int64(b.peBuilder.GetTimestamp())+intervals.EVENTS_PAGE_EVENT_TIMEOUT < ts {
		b.buildPageEvent()
	}
	if b.ieBuilder.HasInstance() && int64(b.ieBuilder.GetTimestamp())+intervals.EVENTS_INPUT_EVENT_TIMEOUT < ts {
		b.buildInputEvent()
	}
	if b.ptaBuilder.HasInstance() && int64(b.ptaBuilder.GetStartTimestamp())+intervals.EVENTS_PERFORMANCE_AGGREGATION_TIMEOUT < ts {
		b.buildPerformanceTrackAggr()
	}

	lastTsGap := ts - int64(b.timestamp)
	//log.Printf("checking timeouts for sess %v: %v now, %v sesstime; gap %v",b.sid,  ts, b.timestamp, lastTsGap)
	if lastTsGap > intervals.EVENTS_SESSION_END_TIMEOUT {
		if rm := b.ddDetector.Build(); rm != nil {
			b.appendReadyMessage(rm)
		}
		if rm := b.ciFinder.Build(); rm != nil {
			b.appendReadyMessage(rm)
		}
		if rm := b.miFinder.Build(); rm != nil {
			b.appendReadyMessage(rm)
		}
		if rm := b.crDetector.Build(); rm != nil {
			b.appendReadyMessage(rm)
		}
		if rm := b.dcDetector.HandleReaction(b.timestamp); rm != nil {
			b.appendReadyMessage(rm)
		}
		b.buildSessionEnd()
		return true
	}
	return false
}
