module matrix

import x.json2
import net
import net.http
import net.urllib
import setup
import rand
import regex
import time
import util

enum ConnState {
	disconnected
	connected
}

type Payload = Connect | Disconnect | JoinRoom | MakeUser

struct Connect {}

struct Disconnect {}

pub struct MakeUser {
pub:
	user_id string
	name    string
}

pub fn (self MakeUser) str() string {
	return '$self.user_id ($self.name)'
}

pub struct JoinRoom {
pub:
	name string
	room string
}

struct Actor {
pub:
	out   chan Say
	cin   chan Payload
	host  string
	token string
	owner string
pub mut:
	conn_state   ConnState
	last_say     time.Time
	joined_rooms Rooms
	whoami       string
}

struct Rooms {
pub mut:
	rooms []Room
}

[heap]
pub struct Room {
pub:
	id   string
	name util.StringOrNone
pub mut:
	user_ids []string
}

pub struct Say {
pub:
	room    Room
	message string
}

pub struct SayContext {
pub:
	room_id string
	say     chan Say
}

pub fn init(config setup.Config) &Actor {
	mut self := &Actor{
		out: chan Say{}
		cin: chan Payload{cap: 100}
		host: config.matrix_host
		token: config.as_token
		owner: config.matrix_owner
		last_say: time.now()
	}
	return self
}

pub fn (mut self Actor) setup() {
	if matrix_id := self.whoami() {
		self.whoami = matrix_id
		makeuser := MakeUser{
			user_id: matrix_id
			name: split(matrix_id)[1]
		}
		self.register(makeuser) or {}
		self.user_presence(matrix_id, 'online') or {}
		self.conn_state = ConnState.connected
		self.cin <- Payload(Connect{})
	} else {
		println('matrix setup failed. please verify the matrix_host and as_token in config.json')
	}
}

pub fn room_from_db(cols []string) &Room {
	return &Room{
		id: cols[0]
		name: util.StringOrNone(cols[1])
	}
}

pub fn rooms_subtract(rooms_a []&Room, rooms_b []&Room) []&Room {
	mut missing := []&Room{}
	for room in rooms_a {
		if rooms_contains(rooms_b, room) {
		} else {
			missing << room
		}
	}
	return missing
}

pub fn rooms_contains(rooms []&Room, looking Room) bool {
	for room in rooms {
		if room.id == looking.id {
			return true
		}
	}
	return false
}

pub fn (mut self Actor) listen() {
}

pub fn (mut self Actor) try_pause() {
	duration := time.now() - self.last_say
	if duration.milliseconds() < 100 {
		println('matrix.say 500ms pause (last call was ${duration.milliseconds()}ms ago)')
		time.sleep(500 * time.millisecond)
	}
	self.last_say = time.now()
}

pub fn (mut self Actor) call_get(api string) ?(map[string]json2.Any, int) {
	return self.call(http.Method.get, api, '')
}

pub fn (mut self Actor) call(method http.Method, api string, body string) ?(map[string]json2.Any, int) {
	// try_pause()
	mut config := http.FetchConfig{
		method: method
		data: body
	}
	config.header.set_custom('Authorization', 'Bearer $self.token') or {
		return error('call http header err $err')
	}
	url := 'https://$self.host/_matrix/client/r0/$api'
	resp := http.fetch(url, config) or { return error('$url $err') }
	println('$method $url $config.data => [$resp.status_code] $resp.text')
	any := json2.raw_decode(resp.text) ?
	return any.as_map(), resp.status_code
}

pub fn (mut self Actor) whoami() ?string {
	kv, _ := self.call_get('account/whoami') or {
		println('whoami fail $err')
		return error('z')
	}
	return kv['user_id'] as string
}

pub fn split(id string) []string {
	mut parts := []string{}
	parts << id.substr(0, 1)
	left := id.substr(1, id.len)
	parts << left.before(':')
	parts << left.after(':')
	return parts
}

pub fn join(parts []string) string {
	return '@' + parts[0] + ':' + parts[1]
}

pub fn (mut self Actor) register(user MakeUser) ?string {
	mut user_data := map[string]json2.Any{}
	username := split(user.user_id)[1]
	user_data['username'] = username
	user_data['type'] = 'm.login.application_service'
	kv, _ := self.call(http.Method.post, 'register', user_data.str()) ?
	if 'errcode' in kv {
		return error('matrix.register() error! $kv')
	} else {
		user_id := kv['user_id'] as string
		self.user_displayname(user_id, user.name) or { println('ERR: $err') }
		return user_id
	}
}

pub fn (mut self Actor) sync() string {
	self.call_get('sync') or {}
	return ''
}

pub fn (mut self Actor) sync_user(user_id string) string {
	self.call_get('sync?user_id=$user_id') or {}
	return ''
}

pub fn (mut self Actor) joined_rooms() ?[]string {
	resp, _ := self.call_get('joined_rooms') ?
	return resp['joined_rooms'].arr().map(it.str())
}

pub fn (mut self Actor) join(room_id string) ?(map[string]json2.Any, int) {
	return self.join_as('', room_id)
}

pub fn (mut self Actor) join_as(user_id string, room_id string) ?(map[string]json2.Any, int) {
	user_part := if user_id.len > 0 { '?user_id=$user_id' } else { '' }
	return self.call(http.Method.post, 'rooms/$room_id/join$user_part', '')
}

pub fn (mut self Actor) leave(room_id string) {
	params, code := self.room_leave(room_id) or { return }
	println('matrix.leave $code $params')
	match code {
		200 {
			self.joined_rooms.delete(room_id)
		}
		404 {
			if params['errcode'].str() == 'M_UNKNOWN' {
				self.joined_rooms.delete(room_id)
			}
		}
		else {}
	}
}

pub fn (mut self Actor) room_leave(room_id string) ?(map[string]json2.Any, int) {
	return self.call(http.Method.post, 'rooms/$room_id/leave', '')
}

// not implemented in synapse until 1.36 (api r0.6.1)
pub fn (mut self Actor) room_aliases(room_id string) ?[]string {
	params, _ := self.call_get('rooms/$room_id/aliases') ?
	return params['aliases'].arr().map(it.str())
}

pub fn (mut self Actor) room_joined_members(room_id string) ?[]string {
	mut members := []string{}
	params, code := self.call_get('rooms/$room_id/joined_members') or { return err }
	if code == 200 {
		for k, _ in params['joined'].as_map() {
			members << k
		}
	}
	return members
}

pub fn (mut self Actor) room_create(room_alias string) ?&Room {
	mut user_data := map[string]json2.Any{}
	user_data['room_alias_name'] = room_alias
	user_data['is_direct'] = true
	params, code := self.call(http.Method.post, 'createRoom', user_data.str()) or { return err }
	if code == 200 {
		room := &Room{
			id: params['room_id'].str()
			name: room_alias
		}
		return room
	} else {
		return error('code $code')
	}
}

pub fn (mut self Actor) room_state(room_id string) ?string {
	self.call_get('rooms/$room_id/state') ?
	return ''
}

pub fn (mut self Actor) room_messages(room Room) string {
	self.call_get('rooms/$room.id/messages') or {}
	return ''
}

pub fn (mut self Actor) room_say(room Room, msg string) string {
	self.room_say_as('', room, msg)
	return ''
}

pub fn (mut self Actor) room_invite(room Room, user string) ?bool {
	mut user_data := map[string]json2.Any{}
	user_data['user_id'] = user
	self.call(http.Method.post, 'rooms/$room.id/invite', user_data.str()) or { return err }
	return true
}

pub enum RoomSayAsReturn {
	good
	user_not_found
	not_in_room
	error
}

pub fn (mut self Actor) room_say_as(user_id string, room Room, msg string) RoomSayAsReturn {
	id := rand.string(6)
	mut evt := map[string]json2.Any{}
	if ctcp_msg := util.ctcp_decode(msg) {
		evt['msgtype'] = 'm.emote'
		evt['body'] = ctcp_msg.split(' ')[1..].join(' ')
	} else {
		evt['msgtype'] = 'm.text'
		evt['body'] = msg
	}
	user_part := if user_id.len > 0 { '?user_id=$user_id' } else { '' }
	resp, code := self.call(http.Method.put, 'rooms/$room.id/send/m.room.message/$id$user_part',
		evt.str()) or {
		println(err)
		return RoomSayAsReturn.error
	}
	if code == 403 {
		error := resp['error'].str()
		//{"errcode":"M_FORBIDDEN","error":"Application service has not registered this user"}
		if error.contains('has not registered') {
			return RoomSayAsReturn.user_not_found
		} else if error.contains('not in room') {
			//{"errcode":"M_FORBIDDEN","error":"User @ircbr_dpdp144:donp.org not in room !FMIqCsoGJDbtjiBptb:donp.org (None)"}
			return RoomSayAsReturn.not_in_room
		}
	}
	return RoomSayAsReturn.good
}

pub fn (mut self Actor) user_presence(user_id string, status string) ?string {
	mut evt := map[string]json2.Any{}
	evt['presence'] = status
	self.call(http.Method.put, 'presence/$user_id/status', evt.str()) ?
	return ''
}

pub fn (mut self Actor) user_display_name(user_id string) ?string {
	escaped_user_id := urllib.path_escape(user_id)
	url := 'profile/$escaped_user_id/displayname?user_id=$escaped_user_id'
	params, _ := self.call_get(url) ?
	displayname := params['displayname'].str()
	return displayname
}

pub fn (mut self Actor) user_displayname(user_id string, displayname string) ?bool {
	mut name_data := map[string]json2.Any{}
	escaped_user_id := urllib.path_escape(user_id)
	url := 'profile/$escaped_user_id/displayname?user_id=$escaped_user_id'
	name_data['displayname'] = displayname
	_, status := self.call(http.Method.put, url, name_data.str()) ?
	return status == 200
}

struct RoomAliasErrNotFound {
pub: // look like IError
	msg  string
	code int
}

pub fn (mut self Actor) room_alias(room_alias string) ?&Room {
	if room_alias.starts_with('#') {
		ret, code := self.call_get('directory/room/' + urllib.path_escape(room_alias)) ?
		if code == 404 {
			return IError(&RoomAliasErrNotFound{})
		} else {
			return &Room{
				name: room_alias
				id: ret['room_id'].str()
			}
		}
	} else {
		return error('$room_alias is not a room_alias')
	}
}

pub fn (self Actor) is_room(room string) bool {
	//#veloren:matrix.org
	room_match := r'^[!#].*:.*$'
	mut re := regex.regex_opt(room_match) or { panic('regex fail') }
	start, _ := re.match_string(room)
	return start > -1
}

pub fn (self Actor) invited_by(data map[string]json2.Any) string {
	mut alias := ''
	for s in data['invite_room_state'].arr() {
		state := s.as_map()
		mtype := state['type'].str()
		println('invite_by checking type $mtype in $state')
		if mtype == 'm.room.canonical_alias' {
			alias = state['content'].as_map()['alias'].str()
			println('invited by room $alias')
		}
		if mtype == 'm.room.member' {
			if alias.len == 0 {
				alias = state['sender'].str()
				println('invited by user $alias')
			}
		}
	}
	if alias.len == 0 {
		println('invite_by: warning no inviter found')
	}
	return alias
}

pub fn (mut self Rooms) replace(rooms []&Room) {
	self.rooms.clear()
	// VBUG self.rooms << rooms
	for r in rooms {
		self.rooms << r
	}
}

pub fn (mut self Rooms) add(room Room) {
	self.rooms << room
}

pub fn (mut self Rooms) delete(room_id string) {
	if idx := self.find_idx_by_id(room_id) {
		self.rooms.delete(idx)
		println('rooms.delete() $room_id')
	} else {
		println('rooms.delete() $room_id $err')
	}
}

pub fn (mut self Rooms) find_idx_by_id(room_id string) ?int {
	for idx, room in self.rooms {
		if room.id == room_id {
			return idx
		}
	}
	return error('not found')
}

pub fn (mut self Rooms) len() int {
	return self.rooms.len
}

pub fn (mut self Rooms) dm(user string) ?Room {
	return self.find_room_by_name(user)
}

pub fn (mut self Rooms) find_room_by_name(name string) ?Room {
	for room in self.rooms {
		if room.name is string { // VBUG room_name := room.name {
			if room.name == name {
				return room
			}
		}
	}
	return error("room name \"$name\" not found")
}

pub fn (mut self Rooms) find_room_by_id(room_id string) ?Room {
	for room in self.rooms {
		if room.id == room_id {
			return room
		}
	}
	return error("room \"$room_id\" not found")
}

pub fn (mut self Rooms) room_by_partial_name(room_name string) ?&Room {
	println('room_name_by_partial_name: searching $room_name in $self.rooms.len rooms')
	for mut room in self.rooms {
		if room.name is string { // this_room_name := room.name {
			partial_name := room.name.before(':')
			println('room_name comparing $partial_name to $room_name')
			if partial_name == room_name {
				return room
			}
		}
	}
	return error('room_partial_name not in room list')
}

pub fn (mut self Rooms) by_id(room_id string) ?&Room {
	for mut room in self.rooms {
		if room.id == room_id {
			return room
		}
	}
	return error('$room_id not in room list')
}

pub fn (self Rooms) pointers() []&Room {
	mut ptrs := []&Room{}
	for mut room in self.rooms {
		ptrs << room
	}
	return ptrs
}

pub fn (self Rooms) str() string {
	return self.rooms.str()
}

pub fn (self Room) str() string {
	name := if self.name is string { self.name } else { 'None' }
	return '"$name"($self.id)'
}