package libnmdc

import (
	"encoding/base32"
	"fmt"
	"regexp"
	"strconv"
	"strings"
)

type adcState int

const (
	adcStateProtocol adcState = 0
	adcStateIdentify adcState = 1
	adcStateVerify   adcState = 2
	adcStateNormal   adcState = 3
	adcStateData     adcState = 4
)

type AdcProtocol struct {
	hc            *HubConnection
	state         adcState
	sid, pid, cid string // all in base32 encoding
	supports      map[string]struct{}
}

const (
	// extra extensions that aren't flagged in SUPPORTS
	adcSeparateApVe string = "SEPARATE_AP_VE" // we invented this string
)

func NewAdcProtocol(hc *HubConnection) Protocol {
	proto := AdcProtocol{
		hc:       hc,
		state:    adcStateProtocol,
		supports: make(map[string]struct{}),
	}

	rxPid := regexp.MustCompile("^[A-Z2-7]{39}$")
	if !rxPid.MatchString(hc.Hco.AdcPID) {
		hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Invalid custom PID, regenerating"})
		hc.Hco.AdcPID = NewPID()
	}

	pid_base32 := hc.Hco.AdcPID
	cid_base32, err := proto.pid2cid(pid_base32)
	if err != nil {
		panic(err)
	}

	proto.cid = cid_base32
	proto.pid = pid_base32

	// Start logging in
	hc.SayRaw("HSUP ADBASE ADTIGR ADUCMD\n")

	return &proto
}

func (this *AdcProtocol) pid2cid(pid_base32 string) (string, error) {

	pid_raw, err := base32.StdEncoding.DecodeString(pid_base32 + "=")
	if err != nil {
		return "", err
	}

	cid_raw := Tiger(string(pid_raw))
	cid_base32 := Base32(cid_raw)

	return cid_base32, nil
}

func (this *AdcProtocol) SID2Nick(sid string) (string, bool) {
	this.hc.usersMut.Lock()
	defer this.hc.usersMut.Unlock()

	nick, ok := this.hc.userSIDs[sid]
	return nick, ok
}

func (this *AdcProtocol) Nick2SID(targetNick string) (string, bool) {
	this.hc.usersMut.Lock()
	defer this.hc.usersMut.Unlock()

	for sid, nick := range this.hc.userSIDs {
		if nick == targetNick {
			return sid, true
		}
	}

	return "", false
}

func (this *AdcProtocol) ProcessCommand(msg string) {

	if len(msg) == 0 {
		return
	}

	this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: msg})

	parts := strings.Split(msg, " ")
	switch parts[0] {

	case "ISUP":
		if !(this.state == adcStateProtocol || this.state == adcStateNormal) {
			this.malformed(parts)
			return
		}
		for _, supportflag := range parts[1:] {
			if len(supportflag) < 2 {
				this.malformed(parts)
				return
			}

			if supportflag[0:2] == "AD" {
				this.supports[supportflag[2:]] = struct{}{}
			} else if supportflag[0:2] == "RM" {
				delete(this.supports, supportflag[2:])
			} else {
				this.malformed(parts)
				return
			}
		}
		if this.state == adcStateProtocol {
			this.state = adcStateIdentify
		}

	case "ISID":
		if this.state != adcStateIdentify {
			this.malformed(parts)
			return
		}
		this.sid = parts[1]

		// State transition IDENTIFY --> VERIFY and send our own info
		this.SayInfo()
		this.state = adcStateVerify

	case "IINF":
		// Hub telling information about itself
		// ADCH++ sends this once we are successfully logged in

		flags, err := this.parts2flags(parts[1:])
		if err != nil {
			this.logError(err)
			return
		}

		if flags["CT"] != "32" {
			this.malformed(parts)
			return
		}

		err = this.handleHubInfo(flags)
		if err != nil {
			this.logError(err)
			return
		}

		if this.state != adcStateNormal {
			this.enterNormalState() // successful login
		}

	case "BINF":
		if this.state != adcStateNormal {
			this.enterNormalState() // successful login
		}

		sid := parts[1]
		flags, err := this.parts2flags(parts[2:])
		if err != nil {
			this.logError(err)
			return
		}

		// Log this user in, and associate this SID with this user
		this.hc.usersMut.Lock()
		defer this.hc.usersMut.Unlock()

		oldNick, sidExists := this.hc.userSIDs[sid]

		uinfo := UserInfo{}
		if sidExists {
			uinfo_lookup, ok := this.hc.users[oldNick]
			if !ok {
				// Shouldn't happen
				this.hc.processEvent(HubEvent{
					EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN,
					Message:   fmt.Sprintf("Hub connection corrupted (missing info for SID='%s' nick='%s'), disconnecting", sid, oldNick),
				})
				this.hc.Disconnect()
				return
			}

			uinfo = uinfo_lookup
		}

		this.updateUserInfo(&uinfo, flags)
		newNick := uinfo.Nick
		if len(newNick) == 0 {
			this.logError(fmt.Errorf("Zero-length nick for user (SID='%s')", sid))
		}

		shouldHandleNewUser := false
		if sidExists && oldNick != newNick {
			// Nick change = delete all trace of this user first, treat as new

			delete(this.hc.users, oldNick)
			delete(this.hc.userSIDs, sid)
			this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: oldNick})
			shouldHandleNewUser = true

		} else if sidExists && oldNick == newNick {
			// Updating existing user
			this.hc.users[newNick] = uinfo
			this.hc.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: newNick})

		} else if !sidExists {
			// User joined
			shouldHandleNewUser = true
		}

		//

		if shouldHandleNewUser {
			// Install this SID as pointing to this nick
			this.hc.userSIDs[sid] = uinfo.Nick

			// Check if this nick was in use by any other SID already
			for otherSid, otherSidNick := range this.hc.userSIDs {
				if otherSidNick == newNick && otherSid != sid {
					this.hc.processEvent(HubEvent{
						EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN,
						Message:   fmt.Sprintf("Hub connection corrupted (duplicate SIDs '%s' and '%s' for nick '%s'), disconnecting", sid, otherSid, newNick),
					})
					this.hc.Disconnect()
					return
				}
			}

			// Notifications
			this.hc.users[newNick] = uinfo
			this.hc.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: newNick})
		}

	case "IMSG":
		// General message from the hub
		if len(parts) < 2 {
			this.malformed(parts)
			return
		}

		this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: this.unescape(parts[1])})

	case "ISTA":
		// Error message from the hub
		if len(parts) < 3 {
			this.malformed(parts)
			return
		}

		code, _ := strconv.Atoi(parts[1])
		msg := this.unescape(parts[2])
		this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: this.ErrorMessage(code, msg)})

	case "IQUI":
		// Error message from the hub
		// IQUI V3M6 DI1 MSNick\staken,\splease\spick\sanother\sone TL-1
		if len(parts) < 2 {
			this.malformed(parts)
			return
		}

		sid := parts[1]

		flags, err := this.parts2flags(parts[2:])
		if err != nil {
			return
		}

		if sid == this.sid {
			if msg, ok := flags["MS"]; ok {
				this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: "The hub is closing our connection because: " + this.unescape(msg)})
			} else {
				this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: "The hub is closing our connection"})
			}
		} else {
			this.hc.usersMut.Lock()
			defer this.hc.usersMut.Unlock()
			otherSidNick, ok := this.hc.userSIDs[sid]
			if ok {
				delete(this.hc.userSIDs, sid)
				delete(this.hc.users, otherSidNick)
				this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: otherSidNick})
			} else {
				// ??
				this.logError(fmt.Errorf("An unknown user quit the hub (SID=%s)", sid))
			}
		}

	case "BMSG":
		// Message from a user
		// BMSG ZVF4 hi
		if len(parts) < 3 {
			this.malformed(parts)
			return
		}

		sid := this.unescape(parts[1])
		msg := this.unescape(parts[2])

		this.hc.usersMut.Lock()
		defer this.hc.usersMut.Unlock()
		nick, ok := this.hc.userSIDs[sid]
		if !ok {
			this.logError(fmt.Errorf("Recieved message from unknown SID '%s'", sid))
			return
		}

		this.hc.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: nick, Message: msg})

	case "IGPA":
		// Password is needed
		// IGPA 7EIAAAECLMAAAPJQAAADQQYAAAWAYAAAKVFQAAF6EAAAAAYFAAAA
		// HPAS LZDIJOTZDPWHINHGPT5RHT6WLU7DRME7DQO2O3Q
		if len(parts) < 2 {
			this.malformed(parts)
			return
		}

		/*
			For GPA/PAS, assuming that '12345' is the random data supplied in GPA, then;
			PAS = Base32( Hash( password + '12345' ) )

			GPA: The data parameter is at least 24 random bytes (base32 encoded).
		*/

		data_base32 := parts[1]
		if len(data_base32)%8 != 0 {
			data_base32 += strings.Repeat("=", 8-(len(data_base32)%8))
		}

		data_raw, err := base32.StdEncoding.DecodeString(data_base32)
		if err != nil {
			this.logError(err)
			return
		}

		resp := Base32(Tiger(this.hc.Hco.NickPassword + string(data_raw)))
		this.hc.SayRaw("HPAS " + resp + "\n")

	case "EMSG":
		// Private message from other user
		// EMSG I5RO FMWH test\spm PMI5RO
		// EMSG sender recip==us message [flags...]
		if len(parts) < 4 {
			this.malformed(parts)
			return
		}

		if parts[2] != this.sid {
			this.logError(fmt.Errorf("Recieved a PM intended for someone else (got SID=%s expected SID=%s)", parts[2], this.sid))
			return
		}

		senderSid := parts[1]
		senderNick, ok := this.SID2Nick(parts[1])
		if !ok {
			this.logError(fmt.Errorf("Recieved a PM from an unknown user (SID=%s)", senderSid))
			return
		}

		msg := this.unescape(parts[3])
		this.hc.processEvent(HubEvent{EventType: EVENT_PRIVATE, Nick: senderNick, Message: msg})

	case "ICMD":
		// Usercommand
		// ICMD ADCH++/About\sthis\shub TTHMSG\s+about\n CT3

		if len(parts) < 2 {
			this.malformed(parts)
			return
		}

		uc := UserCommand{
			Message: this.unescape(parts[1]),
			Type:    USERCOMMAND_TYPE_RAW, // default
		}

		flags, err := this.parts2flags(parts[2:])
		if err != nil {
			this.malformed(parts)
			return
		}

		if ct, ok := flags["CT"]; ok {
			ct64, _ := strconv.ParseUint(ct, 10, 64)
			uc.Context = UserCommandContext(ct64)
		}

		if tt, ok := flags["TT"]; ok {
			uc.Command = tt
		}

		if sp, ok := flags["SP"]; ok && sp == "1" {
			uc.Type = USERCOMMAND_TYPE_SEPARATOR
		}

		if co, ok := flags["CO"]; ok && co == "1" {
			uc.Type = USERCOMMAND_TYPE_NICKLIMITED // "Constrained" in ADC parlance
		}

		if rm, ok := flags["RM"]; ok && rm == "1" {
			uc.RemoveThis = true
		}

		this.hc.processEvent(HubEvent{EventType: EVENT_USERCOMMAND, UserCommand: &uc})

	// Ignored messages
	// ````````````````

	case "DCTM": // Client-client ConnectToMe
	case "BSCH": // Search

	default:
		this.malformed(parts)
	}
}

func (this *AdcProtocol) infoFlagsFor(u *UserInfo) map[string]string {
	parts := map[string]string{
		"NI": u.Nick,
		"SS": fmt.Sprintf("%d", u.ShareSize),
		"SF": fmt.Sprintf("%d", u.SharedFiles),
		"US": fmt.Sprintf("%d", u.UploadSpeedBps),
		"DS": fmt.Sprintf("%d", u.DownloadSpeedBps),
		"SL": fmt.Sprintf("%d", u.Slots),
		"HN": fmt.Sprintf("%d", u.HubsUnregistered),
		"HR": fmt.Sprintf("%d", u.HubsRegistered),
		"HO": fmt.Sprintf("%d", u.HubsOperator),
	}

	if _, ok := this.supports[adcSeparateApVe]; ok {
		parts["AP"] = u.ClientTag
		parts["VE"] = u.ClientVersion
	} else {
		parts["VE"] = fmt.Sprintf("%s %s", u.ClientTag, u.ClientVersion)
	}

	// Do not send the hub a CT (it decides what type we are)

	return parts
}

func (this *AdcProtocol) ourINFO(includePid bool) string {
	parts := this.infoFlagsFor(this.hc.Hco.Self)
	parts["ID"] = this.cid
	if includePid {
		parts["PD"] = this.pid
	}

	ret := ""
	for k, v := range parts {
		ret += " " + k + this.escape(v)
	}
	return ret[1:]
}

func (this *AdcProtocol) SayInfo() error {
	return this.hc.SayRaw("BINF " + this.escape(this.sid) + " " + this.ourINFO(true) + "\n")
}

func (this *AdcProtocol) parts2flags(parts []string) (map[string]string, error) {
	flags := make(map[string]string, len(parts))
	for _, flag := range parts {
		if len(flag) < 2 {
			return nil, fmt.Errorf("Malformed flag '%s'", flag)
		}
		flags[flag[0:2]] = this.unescape(flag[2:])
	}

	return flags, nil
}

func (this *AdcProtocol) handleHubInfo(flags map[string]string) error {
	if flags["CT"] != "32" {
		return fmt.Errorf("Expected CT==32")
	}

	// IINF DEADCH++\sTest\shub VE2.12.1\s(r"[unknown]")\sRelease HI1 NIADCH++ APADCH++ CT32
	// AP: extension 3.24 "Application and version separation in INF"
	// HI:

	// Hub properties updated

	// Special SUPPORT that is only indicated in IINF
	if _, ok := flags["AP"]; ok {
		this.supports[adcSeparateApVe] = struct{}{}
	}

	// Hub's name is in "NI", hub description in "DE"
	hubName, ok := flags["NI"]
	if ok {
		if hubDesc, ok := flags["DE"]; ok && len(hubDesc) > 0 {
			hubName += " - " + hubDesc
		}

		this.hc.HubName = hubName
		this.hc.processEvent(HubEvent{EventType: EVENT_HUBNAME_CHANGED, Nick: this.hc.HubName})
	}

	return nil
}

func (this *AdcProtocol) updateUserInfo(u *UserInfo, flags map[string]string) {

	// User MyINFO
	// BINF GUPR IDFEARIFD33NTGC4YBEZ3UFQS5R4ZXXTFL2QN2GRY PDZMIFLG5EKZG3BDRRMIJPG7ARNA6KW3JVIH3DF7Q NIivysaur5 SL3 FS3 SS0 SF0 HN1 HR0 HO0 VEEiskaltDC++\s2.2.9 US2621440 KPSHA256/3UPRORG4BLJ4CG6TO6R3G75A67LXOGD437NALQALRWJF6XBOECTA I40.0.0.0 U418301
	// BINF GUPR I4172.17.0.1 U418301 IDFEARIFD33NTGC4YBEZ3UFQS5R4ZXXTFL2QN2GRY VEEiskaltDC++\s2.2.9 SF0 NIivysaur5 SL3 HN1 HO0 KPSHA256/3UPRORG4BLJ4CG6TO6R3G75A67LXOGD437NALQALRWJF6XBOECTA HR0 FS3 SS0 US2621440 SUSEGA,ADC0,TCP4,UDP4
	// Or maybe only incremental:
	// BINF Z3BA HO1
	// TODO

	for prop, val := range flags {
		switch prop {
		case "ID":
			u.CID = val
		case "PD":
		// ignore PID - it will only appear if we're talking about our own user
		case "NI":
			u.Nick = val
		case "SL":
			u.Slots, _ = strconv.ParseUint(val, 10, 64)
		case "SS":
			u.ShareSize, _ = strconv.ParseUint(val, 10, 64)
		case "SF":
			u.SharedFiles, _ = strconv.ParseUint(val, 10, 64)
		case "HN":
			u.HubsUnregistered, _ = strconv.ParseUint(val, 10, 64)
		case "HR":
			u.HubsRegistered, _ = strconv.ParseUint(val, 10, 64)
		case "HO":
			u.HubsOperator, _ = strconv.ParseUint(val, 10, 64)
		case "US":
			u.UploadSpeedBps, _ = strconv.ParseUint(val, 10, 64)
		case "DS":
			u.DownloadSpeedBps, _ = strconv.ParseUint(val, 10, 64)
		case "KP":
			u.Keyprint = val
		case "I4":
			u.IPv4Address = val
		case "I6":
			u.IPv6Address = val
		case "U4":
			u.IPv4UDPPort, _ = strconv.ParseUint(val, 10, 64)
		case "U6":
			u.IPv6UDPPort, _ = strconv.ParseUint(val, 10, 64)
		case "SU":
			u.SupportFlags = make(map[string]struct{})
			for _, supportFlag := range strings.Split(val, ",") {
				u.SupportFlags[supportFlag] = struct{}{}
			}
		}
	}

	// VE / AP
	AP, hasAP := flags["AP"]
	VE, hasVE := flags["VE"]
	if hasAP && hasVE {
		u.ClientTag = AP
		u.ClientVersion = VE
	} else if hasAP && !hasVE {
		u.ClientTag, u.ClientVersion = this.getAPVEFromSingle(AP)
	} else if !hasAP && hasVE {
		u.ClientTag, u.ClientVersion = this.getAPVEFromSingle(VE)
	}
}

func (this *AdcProtocol) getAPVEFromSingle(term string) (string, string) {
	words := strings.Split(term, " ")
	if len(words) > 1 {
		return strings.Join(words[0:len(words)-1], " "), words[len(words)-1]
	} else {
		return term, "0"
	}
}

func (this *AdcProtocol) enterNormalState() {
	this.state = adcStateNormal
	this.hc.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED})
	this.hc.State = CONNECTIONSTATE_CONNECTED
}

func (this *AdcProtocol) malformed(parts []string) {
	this.logError(fmt.Errorf("Ignoring malformed, unhandled, or out-of-state protocol command %v", parts))
}

func (this *AdcProtocol) logError(e error) {
	this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Protocol error: " + e.Error()})
}

func (this *AdcProtocol) escape(plaintext string) string {
	// The string "\s" escapes space, "\n" newline and "\\" backslash. This version of the protocol reserves all other escapes for future use; any message containing unknown escapes must be discarded.
	v1 := strings.Replace(plaintext, `\`, `\\`, -1)
	v2 := strings.Replace(v1, "\n", `\n`, -1)
	return strings.Replace(v2, " ", `\s`, -1)
}

func (this *AdcProtocol) unescape(encoded string) string {
	v1 := strings.Replace(encoded, `\s`, " ", -1)
	v2 := strings.Replace(v1, `\n`, "\n", -1)
	return strings.Replace(v2, `\\`, `\`, -1)
}

func (this *AdcProtocol) SayPublic(msg string) error {
	return this.hc.SayRaw("BMSG " + this.sid + " " + this.escape(msg) + "\n")
}

func (this *AdcProtocol) SayPrivate(user, message string) error {
	sid, ok := this.Nick2SID(user)
	if !ok {
		return fmt.Errorf("Unknown user '%s'", user)
	}

	return this.hc.SayRaw("DMSG " + this.sid + " " + sid + " " + this.escape(message) + "\n")
}

func (this *AdcProtocol) ProtoMessageSeparator() string {
	return "\n"
}

func (this *AdcProtocol) ErrorMessage(code int, msg string) string {
	severity := code / 100
	category := (code % 100) / 10
	cat_sub := (code % 100)

	formatSeverity := func(severity int) string {
		switch severity {
		case 0:
			return "OK"
		case 1:
			return "Warning"
		case 2:
			return "Error"
		default:
			return ""
		}
	}

	formatCategory := func(category int) string {
		switch category {
		case 0:
			return ""
		case 1:
			return "Hub not accepting users"
		case 2:
			return "Login failed"
		case 3:
			return "Access denied"
		case 4:
			return "Protocol error"
		case 5:
			return "Transfer error"
		default:
			return ""
		}
	}

	formatCatSub := func(cat_sub int) string {
		switch cat_sub {
		case 11:
			return "Hub is full"
		case 12:
			return "Hub is disabled"
		case 21:
			return "Invalid nick"
		case 22:
			return "Nick is already in use"
		case 23:
			return "Invalid password"
		case 24:
			return "CID already connected"
		case 25:
			return "Access denied"
		case 26:
			return "Registered users only"
		case 27:
			return "Invalid PID"
		case 31:
			return "Permanently banned"
		case 32:
			return "Temporarily banned"
		default:
			return ""
		}
	}

	parts := make([]string, 0, 4)
	if fs := formatSeverity(severity); len(fs) > 0 {
		parts = append(parts, fs)
	}
	if fc := formatCategory(category); len(fc) > 0 {
		parts = append(parts, fc)
	}
	if fcs := formatCatSub(cat_sub); len(fcs) > 0 {
		parts = append(parts, fcs)
	}
	if len(msg) > 0 {
		parts = append(parts, msg)
	}

	return strings.Join(parts, ": ") + fmt.Sprintf(" (code %d)", code)

}
