from typing import Optional, Any, Tuple
from datetime import datetime, timedelta
from io import BytesIO
from email.parser import Parser
from email.header import decode_header
from email.utils import parsedate_to_datetime
from urllib.parse import unquote, parse_qsl
from pathlib import Path
import re, secrets, base64, random, json, mimetypes, uuid, PyRSS2Gen, settings, util.misc
from markupsafe import Markup
from aiohttp import web

from core import models
from core.backend import Backend, BackendSession
from ..misc import gen_mail_data, format_oim, cid_format, puid_format
from .util import find_element, get_tag_localname, render, preprocess_soap, unknown_soap, bool_to_str

LOGIN_PATH = '/login.srf'
TMPL_DIR = 'front/msn/http/tmpl'
ETC_DIR = 'front/msn/etc'
PP = 'Passport1.4'

def register(app: web.Application) -> None:
	util.misc.add_to_jinja_env(app, 'msn', TMPL_DIR, globals = {
		'date_format': util.misc.date_format,
		'cid_format': cid_format,
		'puid_format': puid_format,
		'bool_to_str': bool_to_str,
		'contact_is_favorite': _contact_is_favorite,
		'datetime': datetime,
	})
	
	# MSN >= 5
	app.router.add_get('/nexus-mock', handle_nexus)
	app.router.add_get('/rdr/pprdr.asp', handle_nexus)
	app.router.add_get(LOGIN_PATH, handle_login)
	app.router.add_get('/svcs/mms/tabs.asp', handle_tabs)
	app.router.add_get('/svcs/mms/portal.asp', handle_portal)
	app.router.add_get('/svcs/mms/ads.asp', handle_msn5ads)
	
	# MSN >= 6
	app.router.add_get('/etc/MsgrConfig', handle_msgrconfig)
	app.router.add_post('/etc/MsgrConfig', handle_msgrconfig)
	# TODO: this is stupid, find a way to make this case-insensitive
	app.router.add_get('/Config/MsgrConfig.asmx', handle_msgrconfig)
	app.router.add_post('/Config/MsgrConfig.asmx', handle_msgrconfig)
	app.router.add_get('/config/MsgrConfig.asmx', handle_msgrconfig)
	app.router.add_post('/config/MsgrConfig.asmx', handle_msgrconfig)
	app.router.add_get('/start', handle_today)
	
	# MSN >= 7.5
	app.router.add_route('OPTIONS', '/NotRST.srf', handle_not_rst)
	app.router.add_post('/NotRST.srf', handle_not_rst)
	app.router.add_post('/RST.srf', handle_rst)
	app.router.add_post('////RST.srf', handle_rst) # libpurple what the FUCK, also this doesn't even work
	app.router.add_post('/RST2.srf', lambda req: handle_rst(req, rst2 = True))
	
	# MSN >= 8
	app.router.add_post('/storageservice/SchematizedStore.asmx', handle_storageservice)
	app.router.add_post('/ppsecure/sha1auth.srf', handle_sha1auth)
	app.router.add_post('/rsi/rsi.asmx', handle_rsi)
	app.router.add_post('/OimWS/oim.asmx', handle_oim)
	app.router.add_get('/svcs/msn-video-feeds/', handle_videofeed) # adding an ending back-slash is required, or else it 404s. ????
	app.router.add_get('/svcs/feed', lambda req: handle_videofeed(req, get_videos = True))

	# MSN >= 14
	app.router.add_post('/whatsnew/whatsnewservice.asmx', handle_whatsnew)
	app.router.add_get('/svcs/tab={id}', handle_tabs_newer)

	# MSN >= 15
	app.router.add_post('/sqm/messenger/sqmserver.dll', handle_blankok)
	app.router.add_post('/sqm/WindowsLive/sqmserver.dll', handle_blankok)
	app.router.add_post('/uploaddata.aspx', handle_blankok)
	app.router.add_get('/ppcrlcheck.srf', handle_ppcrlcheck)
	app.router.add_post('/profile/profile.asmx', handle_blankok) # TODO: do the thing
	app.router.add_get(r'/~Live.ConfigServer/{junk:.*}/~op-GetClientConfig/{tail:.*}', handle_msgrconfig16)
	app.router.add_get(r'/~Live.ConfigServer.SuiteUpdate/{tail:.*}', handle_suiteupdate)
	app.router.add_get(r'/~Live.ConfigServer.PSA/{tail:.*}', handle_socialint)
	app.router.add_get('/svcs/GameBrowser', handle_gamebrowser)

	# Misc
	app.router.add_get('/{i}meen_{locale}/{id}', handle_msn_redirect)

async def handle_blankok(req: web.Request) -> web.Response:
	return web.Response(status = 200)

async def handle_storageservice(req: web.Request) -> web.Response:
	backend = req.app['backend']
	header, action, bs, token = await preprocess_soap(req)
	assert bs is not None
	soapaction = (req.headers.get('SOAPAction') or '')
	if soapaction.startswith('"') and soapaction.endswith('"'):
		soapaction = soapaction[1:-1]
	storage_ns = ('w10' if soapaction.startswith('http://www.msn.com/webservices/storage/w10/') else '2008')
	action_str = get_tag_localname(action)
	now_str = util.misc.date_format(datetime.utcnow())
	user = bs.user
	cachekey = secrets.token_urlsafe(172)
	
	cid = cid_format(user.uuid)
	
	if action_str == 'GetProfile':
		roaming_info = backend.user_service.get_roaming_info(user)
		assert roaming_info is not None
		
		storage_path = _get_storage_path(user.uuid)
		files = None
		if storage_path.exists() and storage_path.is_dir():
			files = [x for x in storage_path.iterdir() if '_thumb' not in x.stem]
		
		mime = None
		image_size = 0
		image_thumb_size = 0
		
		if files:
			ext = files[0].suffix
			mime = ext[1:]
			image_path = storage_path / '{}{}'.format(user.uuid, ext)
			image_thumb_path = storage_path / '{}_thumb{}'.format(user.uuid, ext)
			
			if image_path.exists():
				image_size = image_path.stat().st_size
			
			if image_thumb_path.exists():
				image_thumb_size = image_thumb_path.stat().st_size

		
		return render(req, 'msn:storageservice/GetProfileResponse.xml', {
			'storage_ns': storage_ns,
			'cachekey': cachekey,
			'cid': cid,
			'pptoken1': token,
			'user': user,
			'now': now_str,
			'mime': mime,
			'size_static': image_size,
			'size_small': image_thumb_size,
			'roaming_info': roaming_info,
			'host': settings.USERSTORAGE_HOST,
		})
	if action_str == 'FindDocuments':
		# TODO: FindDocuments
		return render(req, 'msn:storageservice/FindDocumentsResponse.xml', {
			'storage_ns': storage_ns,
			'cachekey': cachekey,
			'cid': cid,
			'pptoken1': token,
			'user': user,
		})
	if action_str == 'UpdateProfile':
		delete_psm = False
		delete_name = False
		delete_avatar = False
		
		# TODO: More properties?
		
		# Update to roaming name/message
		# ```
		# <UpdateProfile xmlns="http://www.msn.com/webservices/storage/w10">
		#   <profile>
		#	 <ResourceID>862d987eb60b7a63!106</ResourceID>
		#	 <ExpressionProfile>
		#	   <FreeText>Update</FreeText>
		#	   <DisplayName>Society is betrayal</DisplayName>
		#	   <PersonalStatus>Prosperity is the best medicine. :)</PersonalStatus>
		#	 </ExpressionProfile>
		#   </profile>
		# </UpdateProfile>
		# ```
		
		# Remove roaming message
		# ```
		# <UpdateProfile xmlns="http://www.msn.com/webservices/storage/w10">
		#   <profile>
		#	 <ResourceID>bb4542ce2eacdbde!106</ResourceID>
		#	 <ExpressionProfile>
		#	   <FreeText>Update</FreeText>
		#	   <DisplayName>%walkingphas3r%</DisplayName>
		#	   <Flags>0</Flags>
		#	 </ExpressionProfile>
		#   </profile>
		#   <profileAttributesToDelete>
		#	 <ExpressionProfileAttributes>
		#	   <PersonalStatus>true</PersonalStatus>
		#	 </ExpressionProfileAttributes>
		#   </profileAttributesToDelete>
		# </UpdateProfile>
		# ```
		
		expression_profile = find_element(action, 'ExpressionProfile')
		name = find_element(expression_profile, 'DisplayName')
		message = find_element(expression_profile, 'PersonalStatus')
		
		attributes_to_delete = find_element(action, 'profileAttributesToDelete/ExpressionProfileAttributes')
		if attributes_to_delete is not None:
			# `PersonalStatus` and `DisplayName` is the only known attribute that has the ability to be deleted
			delete_psm = find_element(attributes_to_delete, 'PersonalStatus') or False
			assert isinstance(delete_psm, bool)
			delete_name = find_element(attributes_to_delete, 'DisplayName') or False
			assert isinstance(delete_name, bool)
		
		name = find_element(action, 'DisplayName')
		message = find_element(action, 'PersonalStatus')
		
		if name:
			backend.user_service.save_single_roaming(user, { 'name': name })
		if message and not delete_psm:
			backend.user_service.save_single_roaming(user, { 'message': message })
		
		if delete_psm:
			backend.user_service.save_single_roaming(user, { 'message': '' })
		if delete_name:
			backend.user_service.save_single_roaming(user, { 'name': '' })
		
		return render(req, 'msn:storageservice/UpdateProfileResponse.xml', {
			'storage_ns': storage_ns,
			'cachekey': cachekey,
			'cid': cid,
			'pptoken1': token,
			'user': user
		})
	if action_str == 'DeleteRelationships':
		# TODO: DeleteRelationships
		return render(req, 'msn:storageservice/DeleteRelationshipsResponse.xml', {
			'storage_ns': storage_ns,
			'cachekey': cachekey,
			'cid': cid,
			'pptoken1': token,
			'user': user,
		})
	if action_str in ('CreateDocument','UpdateDocument'):
		return handle_document(req, action, ('Update' if action_str == 'UpdateDocument' else 'Create'), storage_ns, user, cid, token)
	if action_str == 'CreateRelationships':
		# TODO: CreateRelationships
		return render(req, 'msn:storageservice/CreateRelationshipsResponse.xml', {
			'storage_ns': storage_ns,
			'cachekey': cachekey,
			'cid': cid,
			'pptoken1': token,
			'user': user,
		})
	if action_str in { 'ShareItem' }:
		# TODO: ShareItem
		return unknown_soap(req, header, action, expected = True)
	return unknown_soap(req, header, action)

def handle_document(req: web.Request, action: Any, type: str, storage_ns: str, user: models.User, cid: str, token: str) -> web.Response:
	from PIL import Image
	
	# get image data
	#name = find_element(action, 'Name')
	streamtype = find_element(action, 'DocumentStreamType')
	
	if streamtype == 'UserTileStatic':
		mime = find_element(action, 'MimeType')
		#mime = None
		data = find_element(action, 'Data')
		data = base64.b64decode(data)
		
		# WLM sends either `png` or `image/png` as the MIME type no matter what type of file is sent over. Guess image type from header
		
		# TODO: BMPs
		if data[:6] in (b'GIF87a',b'GIF89a'):
			mime = 'gif'
		elif data[:2] == b'\xff\xd8':
			mime = 'jpeg'
		elif data[:8] == b'\x89PNG\x0d\x0a\x1a\x0a':
			mime = 'png'
		
		if mime is not None:
			try:
				image = Image.open(BytesIO(data))
			except:
				return web.HTTPInternalServerError(text = '')
			
			# store display picture as file
			path = _get_storage_path(user.uuid)
			path.mkdir(exist_ok = True, parents = True)
			
			for old_file in path.glob(f'{user.uuid}.*'):
				if old_file.is_file():
					old_file.unlink()
			for old_thumb in path.glob(f'{user.uuid}_thumb.*'):
				if old_thumb.is_file():
					old_thumb.unlink()
			
			image_path = path / '{uuid}.{mime}'.format(uuid = user.uuid, mime = mime)
			
			image_path.write_bytes(data)
			
			thumb = image.resize((21, 21))
			
			thumb_path = path / '{uuid}_thumb.png'.format(uuid = user.uuid)
			thumb.save(str(thumb_path))
	
	return render(req, 'msn:storageservice/{}DocumentResponse.xml'.format(type), {
		'storage_ns': storage_ns,
		'cid': cid,
		'pptoken1': token,
		'user': user,
	})

async def handle_sha1auth(req: web.Request) -> web.Response:
	# We have no use for any of the actual tokens sent here right now (this is primarily for WLM 8's MSN Today function),
	# so just redirect to the URL specified by `ru`
	post = await req.post()
	
	token_data = post.get('token')
	if token_data is None:
		return web.HTTPInternalServerError()
	
	token_fields = dict(parse_qsl(str(token_data)))
	if 'ru' not in token_fields:
		return web.HTTPInternalServerError()
	
	return web.HTTPFound(token_fields['ru'])

async def handle_ppcrlcheck(req: web.Request) -> web.Response:
	response = '<config>\r\n\t<deviceid minversion="16.000.26889.00"/>\r\n\t<mobilecfg minversion="16.000.26208.0"/>\r\n</config>'
	return web.HTTPOk(body=response)

async def handle_rsi(req: web.Request) -> web.Response:
	_, action, bs, token = await preprocess_soap_rsi(req)
	
	if token is None or bs is None:
		return render(req, 'msn:oim/Fault.validation.xml', status = 500)
	action_str = get_tag_localname(action)
	
	user = bs.user
	
	backend = req.app['backend']
	
	if action_str == 'GetMetadata':
		return render(req, 'msn:oim/GetMetadataResponse.xml', {
			'md': gen_mail_data(user, backend, on_ns = False, e_node = False),
		})
	if action_str == 'GetMessage':
		oim_uuid = find_element(action, 'messageId')
		oim_markAsRead = find_element(action, 'alsoMarkAsRead')
		oim = backend.user_service.get_oim_single(user, oim_uuid, mark_read = oim_markAsRead is True)
		return render(req, 'msn:oim/GetMessageResponse.xml', {
			'oim_data': format_oim(oim),
		})
	if action_str == 'DeleteMessages':
		messageIds = action.findall('.//{*}messageIds/{*}messageId')
		if not messageIds:
			return render(req, 'msn:oim/Fault.validation.xml', status = 500)
		for messageId in messageIds:
			if backend.user_service.get_oim_single(user, str(messageId)) is None:
				return render(req, 'msn:oim/Fault.validation.xml', status = 500)
		for messageId in messageIds:
			backend.user_service.delete_oim(user.uuid, str(messageId))
		bs.evt.msn_on_oim_deletion(len(messageIds))
		return render(req, 'msn:oim/DeleteMessagesResponse.xml')
	
	return render(req, 'msn:Fault.doesnotexist.xml', { 'action': action_str })

async def handle_oim(req: web.Request) -> web.Response:
	header, _, body_content, bs, _ = await preprocess_soap_oimws(req)
	soapaction = (req.headers.get('SOAPAction') or '')
	if soapaction.startswith('"') and soapaction.endswith('"'):
		soapaction = soapaction[1:-1]
	owsns = (
		'http://messenger.msn.com/ws/2004/09/oim/'
		if soapaction.startswith('http://messenger.msn.com/ws/2004/09/oim/')
		else 'http://messenger.live.com/ws/2006/09/oim/'
	)
	
	lockkey_result = header.find('.//{*}Ticket').get('lockkey')
	
	if bs is None or lockkey_result in (None,''):
		return render(req, 'msn:oim/Fault.authfailed.xml', {
			'owsns': owsns,
		}, status = 500)
	
	backend: Backend = req.app['backend']
	user = bs.user
	detail = user.detail
	assert detail is not None
	
	friendlyname = None
	friendlyname_str = None
	friendly_charset = None
	
	friendlyname_mime = header.find('.//{*}From').get('friendlyName')
	email = header.find('.//{*}From').get('memberName')
	recipient = header.find('.//{*}To').get('memberName')
	
	recipient_uuid = backend.util_get_uuid_from_email(recipient)
	
	if email != user.email or recipient_uuid is None or not _is_on_al(recipient_uuid, backend, user, detail):
		return render(req, 'msn:oim/Fault.unavailable.xml', {
			'owsns': owsns,
		}, status = 500)
	
	assert req.transport is not None
	peername = req.transport.get_extra_info('peername')
	if peername:
		host = peername[0]
	else:
		host = '127.0.0.1'
	
	oim_msg_seq = str(find_element(header, 'Sequence/MessageNumber'))
	if not oim_msg_seq.isnumeric():
		return render(req, 'msn:oim/Fault.invalidcontent.xml', {
			'owsns': owsns,
		}, status = 500)
	
	if friendlyname_mime is not None:
		try:
			friendlyname, friendly_charset = decode_header(friendlyname_mime)[0]
		except:
			return render(req, 'msn:oim/Fault.invalidcontent.xml', {
				'owsns': owsns,
			}, status = 500)
	
	if friendly_charset is None:
		friendly_charset = 'utf-8'
	
	if friendlyname is not None:
		friendlyname_str = friendlyname.decode(friendly_charset)
	
	oim_proxy_string = header.find('.//{*}From').get('proxy')
	
	try:
		oim_mime = Parser().parsestr(body_content)
	except:
		return render(req, 'msn:oim/Fault.invalidcontent.xml', {
			'owsns': owsns,
		}, status = 500)
	
	oim_run_id = str(oim_mime.get('X-OIM-Run-Id'))
	if oim_run_id is None:
		return render(req, 'msn:oim/Fault.invalidcontent.xml', {
			'owsns': owsns,
		}, status = 500)
	if not re.match(r'^\{?[A-Fa-f0-9]{8,8}-([A-Fa-f0-9]{4,4}-){3,3}[A-Fa-f0-9]{12,12}\}?', oim_run_id):
		return render(req, 'msn:oim/Fault.invalidcontent.xml', {
			'owsns': owsns,
		}, status = 500)
	oim_run_id = oim_run_id.replace('{', '').replace('}', '')
	if (
		'X-Message-Info', 'Received', 'From', 'To', 'Subject', 'X-OIM-originatingSource', 'X-OIMProxy', 'Message-ID',
		'X-OriginalArrivalTime', 'Date', 'Return-Path'
	) in oim_mime.keys():
		return render(req, 'msn:oim/Fault.invalidcontent.xml', {
			'owsns': owsns,
		}, status = 500)
	if str(oim_mime.get('MIME-Version')) != '1.0':
		return render(req, 'msn:oim/Fault.invalidcontent.xml', {
			'owsns': owsns,
		}, status = 500)
	if not str(oim_mime.get('Content-Type')).startswith('text/plain'):
		return render(req, 'msn:oim/Fault.invalidcontent.xml', {
			'owsns': owsns,
		}, status = 500)
	if str(oim_mime.get('Content-Transfer-Encoding')) != 'base64':
		return render(req, 'msn:oim/Fault.invalidcontent.xml', {
			'owsns': owsns,
		}, status = 500)
	if str(oim_mime.get('X-OIM-Message-Type')) != 'OfflineMessage':
		return render(req, 'msn:oim/Fault.invalidcontent.xml', {
			'owsns': owsns,
		}, status = 500)
	oim_seq_num = str(oim_mime.get('X-OIM-Sequence-Num'))
	if oim_seq_num != oim_msg_seq:
		return render(req, 'msn:oim/Fault.invalidcontent.xml', {
			'owsns': owsns,
		}, status = 500)
	oim_headers = {name: str(value) for name, value in oim_mime.items()}
	
	try:
		i = body_content.index('\n\n') + 2
		oim_body = body_content[i:]
		for oim_b64_line in oim_body.split('\n'):
			if len(oim_b64_line) > 77:
				return render(req, 'msn:oim/Fault.invalidcontent.xml', {
					'owsns': owsns,
				}, status = 500)
		oim_body_normal = oim_body.strip()
		oim_body_normal = base64.b64decode(oim_body_normal).decode('utf-8')
		
		backend.user_service.save_oim(
			bs, recipient_uuid, oim_run_id, host, oim_body_normal, True, from_friendly = friendlyname_str,
			from_friendly_charset = friendly_charset, headers = oim_headers, oim_proxy = oim_proxy_string,
		)
	except:
		return render(req, 'msn:oim/Fault.invalidcontent.xml', {
			'owsns': owsns,
		}, status = 500)
	
	return render(req, 'msn:oim/StoreResponse.xml', {
		'seq': oim_msg_seq,
		'owsns': owsns,
	})

def _is_on_al(uuid: str, backend: Backend, user: models.User, detail: models.UserDetail) -> bool:
	contact = detail.contacts.get(uuid)
	if user.settings.get('BLP', 'AL') == 'AL' and (contact is None or not contact.lists & models.ContactList.BL):
		return True
	if user.settings.get('BLP', 'AL') == 'BL' and contact is not None and not contact.lists & models.ContactList.BL:
		return True
	
	if contact is not None:
		ctc_detail = backend._load_detail(contact.head)
		assert ctc_detail is not None
		
		ctc_me = ctc_detail.contacts.get(user.uuid)
		if ctc_me is None and contact.head.settings.get('BLP', 'AL') == 'AL':
			return True
		if ctc_me is not None and not ctc_me.lists & models.ContactList.BL:
			return True
	return False

async def preprocess_soap_rsi(req: web.Request) -> Tuple[Any, Any, Optional[BackendSession], str]:
	from lxml.objectify import fromstring as parse_xml
	
	body = await req.read()
	root = parse_xml(body)
	
	token_tag = root.find('.//{*}PassportCookie/{*}*[1]')
	if get_tag_localname(token_tag) != 't':
		token = None
	token = token_tag.text
	if token is not None:
		token = token[0:20]
	
	backend: Backend = req.app['backend']
	bs = backend.util_get_sess_by_token(token)
	
	header = find_element(root, 'Header')
	action = find_element(root, 'Body/*[1]')
	#if settings.DEBUG: print('Action: {}'.format(get_tag_localname(action)))
	
	return header, action, bs, token

async def preprocess_soap_oimws(req: web.Request) -> Tuple[Any, str, str, Optional[BackendSession], str]:
	from lxml.objectify import fromstring as parse_xml
	
	body = await req.read()
	root = parse_xml(body)
	
	token = root.find('.//{*}Ticket').get('passport')
	if token[0:2] == 't=':
		token = token[2:22]
	
	backend: Backend = req.app['backend']
	bs = backend.util_get_sess_by_token(token)
	
	header = find_element(root, 'Header')
	body_msgtype = str(find_element(root, 'Body/MessageType'))
	body_content = str(find_element(root, 'Body/Content')).replace('\r\n', '\n')
	
	return header, body_msgtype, body_content, bs, token

# TODO: Make this actually accurate
async def handle_whatsnew(req: web.Request) -> web.Response:
	whatsnew = ''
	with open('config/whatsnew.json', 'rb') as f:
		whatsnew_list = json.loads(f.read()) 
		f.close()
	
	if len(whatsnew_list) > 0:
		whatsnew_dict = whatsnew_list[0]
		with open(TMPL_DIR + '/whatsnew/WhatsNewResponse.xml') as fh:
			whatsnew = fh.read()
		whatsnew = whatsnew.format(
			date=whatsnew_dict['date'],
			url=whatsnew_dict['url'],
			title=whatsnew_dict['title'],
			appName=whatsnew_dict['appName'],
			content=whatsnew_dict['content']
		)
	
	return web.HTTPOk(content_type = 'text/xml', text = whatsnew)

async def handle_today(req: web.Request) -> web.Response:
	return render(req, 'msn:svcs/MSNToday.html', {
		'title': "CrossTalk Today",
		'msn': req.query.get('msn') or False,
		'wlm': req.query.get('wlm') or False,
		'windowslive': req.query.get('windowslive') or False
	})

async def handle_gamebrowser(req: web.Request) -> web.Response:
	return render(req, 'msn:svcs/GameBrowser.html', {
		'title': "Game Browser | CrossTalk",
	})

async def handle_portal(req: web.Request) -> web.Response:
	return web.HTTPFound(f'http://today.msgrsvcs.ctsrv.gay/start/msn')

async def handle_textadredir(req: web.Request) -> web.Response:
	return web.HTTPFound(f'http://ctsvcs.advertising.ugnet.gay/ads/txt')

async def handle_banneradredir(req: web.Request) -> web.Response:
	return web.HTTPFound(f'http://ctsvcs.advertising.ugnet.gay/ads/banner')

async def handle_msn_redirect(req: web.Request) -> web.Response:
	i = req.match_info['i']
	id = req.match_info['id']
	
	if i == '5':
		if id == '60':
			return web.HTTPFound('/svcs/mms/tabs.asp')
		elif id == '153':
			return web.HTTPFound('/start')
	
	return web.HTTPFound('http://g.msn.com{}'.format(req.path_qs))

async def handle_msn5ads(req: web.Request) -> web.Response:
	response = '<?xml version="1.0"?><msn-data><RefreshLogin>False</RefreshLogin><RefreshInterval>10080</RefreshInterval></msn-data>'
	return web.HTTPOk(body=response)

async def handle_suiteupdate(req: web.Request) -> web.Response:
	return render(req, 'msn:config/SuiteUpdate.xml')

async def handle_socialint(req: web.Request) -> web.Response:
	return render(req, 'msn:config/Config.PSA.xml')

async def handle_tabs(req: web.Request) -> web.Response:	
	with open('config/tabs.json') as fh:
		tabs_data = json.load(fh)

	tmpl = req.app['jinja_env'].get_template('msn:svcs/svcs_tabs.xml')
	tab_tmpl = tmpl.render(tabs=tabs_data)

	return web.HTTPOk(content_type='text/xml', text='<?xml version="1.0"?>\n' + tab_tmpl)

# TODO: Unify the tabs handlers so spaghetti code isn't a problem
async def handle_tabs_newer(req: web.Request) -> web.Response:
	id = int(req.match_info['id'])

	with open('config/tabs.json') as fh:
		tabs_data = json.load(fh)

	tab_data = next((tab for tab in tabs_data if tab["id"] == id), None)
	if not tab_data:
		return web.HTTPNotFound(text="Tab not found")

	tmpl = req.app['jinja_env'].get_template('msn:svcs/svcs_tabs_newer.xml')
	tab_tmpl = tmpl.render(tab=tab_data)

	return web.HTTPOk(content_type='text/xml', text=tab_tmpl)

async def handle_msgrconfig(req: web.Request) -> web.Response:
	if req.method == 'POST':
		body = await req.read() # type: Optional[bytes]
	else:
		body = None
	msgr_config = _get_msgr_config(req, body)
	if msgr_config == 'INVALID_VER':
		return web.Response(status = 500)
	return web.HTTPOk(content_type = 'text/xml', text = msgr_config)

async def handle_msgrconfig16(req: web.Request) -> web.Response:
	body = await req.read()
	msgr_config = _get_msgr_config_16(req, body)
	return web.HTTPOk(content_type = 'text/xml', text = msgr_config)

def _get_msgr_config(req: web.Request, body: Optional[bytes]) -> str:
	query = req.query
	result = None # type: Optional[str]
	ver = query.get('ver') or ''
	
	if ver:
		if re.match(r'[^\d\.]', ver):
			return 'INVALID_VER'
		
		config_ver = ver.split('.', 4)
		if 8 <= int(config_ver[0]) <= 9:
			with open(TMPL_DIR + '/config/MsgrConfig.wlm.8.xml') as fh:
				config = fh.read()
			with open('config/tabs.json') as fh:
				with open(TMPL_DIR + '/svcs/svcs_tabs.xml') as th:
					template = th.read()
				tabs_data = json.load(fh)
				tmpl_before = req.app['jinja_env'].from_string(template)
				tab_tmpl = tmpl_before.render(tabs=tabs_data, settings=settings)
			result = config.format(tabs = tab_tmpl)
		elif int(config_ver[0]) >= 14:
			with open(TMPL_DIR + '/config/MsgrConfig.wlm.14.xml') as fh:
				template = fh.read()
			with open('config/tabs.json') as fh:
				tabs_data = json.load(fh)
				tmpl_before = req.app['jinja_env'].from_string(template)
				tmpl = tmpl_before.render(tabs=tabs_data, settings=settings)
				result = tmpl
	elif body is not None:
		with open(TMPL_DIR + '/config/MsgrConfig.msn.envelope.xml') as fh:
			envelope = fh.read()
		with open(TMPL_DIR + '/config/MsgrConfig.msn.xml') as fh:
			config = fh.read()
		with open('config/tabs.json') as fh:
			with open(TMPL_DIR + '/svcs/svcs_tabs.xml') as th:
				template = th.read()
			tabs_data = json.load(fh)
			tmpl_before = req.app['jinja_env'].from_string(template)
			tab_tmpl = tmpl_before.render(tabs=tabs_data)
		result = envelope.format(MsgrConfig = config.format(tabs = tab_tmpl, settings = settings))
	
	return result or ''

def _get_msgr_config_16(req: web.Request, body: Optional[bytes]) -> str:
	with open(TMPL_DIR + '/config/MsgrConfig.wlm.16.xml') as fh:
		template = fh.read()
	with open('config/tabs.json') as fh:
		tabs_data = json.load(fh)
	tmpl_before = req.app['jinja_env'].from_string(template)
	tmpl = tmpl_before.render(tabs=tabs_data, settings=settings)
	result = tmpl

	return result or ''

PassportURLs = 'PassportURLs'

async def handle_nexus(req: web.Request) -> web.Response:
	return web.HTTPOk(headers = {
		PassportURLs: 'DALogin=https://{}{}'.format(settings.LOGIN_HOST, LOGIN_PATH),
	})

async def handle_login(req: web.Request) -> web.Response:
	tmp = _extract_pp_credentials(req.headers.get('Authorization') or '')
	if tmp is None:
		token = None
	else:
		email, pwd = tmp
		token_tpl = _login(req, email, pwd)
	if token_tpl is None:
		raise web.HTTPUnauthorized(headers={
			'WWW-Authenticate': '{}da-status=failed'.format(PP),
		})
	token, _, _ = token_tpl
	return web.HTTPOk(headers={
		'Authentication-Info': '{}da-status=success,from-PP=\'{}\''.format(PP, token),
	})

async def handle_not_rst(req: web.Request) -> web.Response:
	if req.method == 'OPTIONS':
		return web.HTTPOk(headers = {
			'Access-Control-Allow-Origin': '*',
			'Access-Control-Allow-Methods': 'POST',
			'Access-Control-Allow-Headers': 'X-User, X-Password, Content-Type',
			'Access-Control-Expose-Headers': 'X-Token',
			'Access-Control-Max-Age': str(86400),
		})
	
	email = req.headers.get('X-User') or ''
	pwd = req.headers.get('X-Password') or ''
	token_tpl = _login(req, email, pwd, lifetime = 86400)
	headers = {
		'Access-Control-Allow-Origin': '*',
		'Access-Control-Allow-Methods': 'POST',
		'Access-Control-Expose-Headers': 'X-Token',
	}
	if token_tpl is not None:
		token, _, _ = token_tpl
		headers['X-Token'] = token
	return web.HTTPOk(headers = headers)

async def handle_rst(req: web.Request, rst2: bool = False) -> web.Response:
	from lxml.objectify import fromstring as parse_xml
	
	body = await req.read()
	try:
		root = parse_xml(body)
	except:
		return render(req, 'msn:RST/{}.error.xml'.format('RST2' if rst2 else 'RST'))
	
	email = find_element(root, 'Username')
	pwd = str(find_element(root, 'Password'))

	if email is None or pwd is None:
		return render(req, 'msn:RST/{}.error.xml'.format('RST2' if rst2 else 'RST'))
	
	backend: Backend = req.app['backend']
	
	token_tpl = _login(req, email, pwd, binary_secret = True, lifetime = 86400)
	
	uuid = backend.util_get_uuid_from_email(email)
	
	if token_tpl is not None and uuid is not None:
		token, expiry, bsecret = token_tpl
		day_before_expiry = expiry - timedelta(days = 1)
		timez = util.misc.date_format(day_before_expiry)
		tomorrowz = util.misc.date_format(expiry)
		time_5mz = util.misc.date_format((day_before_expiry + timedelta(minutes = 5)))
		
		# load PUID and CID
		cid = cid_format(uuid)
		puid = puid_format(uuid)
		
		assert req.transport is not None
		peername = req.transport.get_extra_info('peername')
		if peername:
			host = peername[0]
		else:
			host = '127.0.0.1'
		
		# get list of requested domains
		domains = root.findall('.//{*}Address')
		
		tmpl = req.app['jinja_env'].get_template('msn:RST/{}.token.xml'.format('RST2' if rst2 else 'RST'))
		# collect tokens for requested domains, ignore Passport token request
		tokenxmls = [tmpl.render(
			i = i + 1,
			domain = domain,
			timez = timez,
			tomorrowz = tomorrowz,
			pptoken1 = token,
			binarysecret = bsecret,
		) for i, domain in enumerate(domains) if domain != 'http://Passport.NET/tb']
		
		tmpl = req.app['jinja_env'].get_template('msn:RST/{}.xml'.format('RST2' if rst2 else 'RST'))
		return web.HTTPOk(
			content_type = 'text/xml',
			text = (tmpl.render(
				puidhex = puid,
				time_5mz = time_5mz,
				timez = timez,
				tomorrowz = tomorrowz,
				cid = cid,
				email = email,
				firstname = "John",
				lastname = "Doe",
				ip = req.headers.get('X-Forwarded-For', req.remote),
				pptoken1 = token,
				tokenxml = Markup(''.join(tokenxmls)),
			) if rst2 else tmpl.render(
				puidhex = puid,
				timez = timez,
				tomorrowz = tomorrowz,
				cid = cid,
				email = email,
				firstname = "John",
				lastname = "Doe",
				ip = req.headers.get('X-Forwarded-For', req.remote),
				pptoken1 = token,
				tokenxml = Markup(''.join(tokenxmls)),
			)),
		)
	
	return render(req, 'msn:RST/{}.authfailed.xml'.format('RST2' if rst2 else 'RST'), {
		'timez': util.misc.date_format(datetime.utcnow()),
	})

def _get_storage_path(uuid: str) -> Path:
	return Path('storage/dp') / uuid[0:1] / uuid[0:2]

async def handle_videofeed(req: web.Request, get_videos: bool = False) -> web.Response:
	if not get_videos:
		return render(req, 'msn:svcs/videofeedroot.xml')
	else:
		with open('config/videos.json', 'rb') as f:
			vid_data = json.load(f)
		rss_items = []
		for video in vid_data:
			try:
				pub_date = parsedate_to_datetime(video['publish_date'])
			except Exception:
				pub_date = datetime.now()
			item = PyRSS2Gen.RSSItem(
				title=video['title'],
				link=video['link'],
				description=video['description'],
				guid=PyRSS2Gen.Guid(video['link'], isPermaLink=True),
				pubDate=pub_date,
				enclosure=PyRSS2Gen.Enclosure(
					url=video['thumbnail'],
					length="1",
					type="image/jpeg"
				)
			)
			rss_items.append(item)
		rss_feed = PyRSS2Gen.RSS2(
			title="CrossTalk",
			link="http://crosstalk.im",
			description="CrossTalk Videos",
			language="en-us",
			lastBuildDate=datetime.now(),
			ttl=10,
			copyright="&copy; 2023 - 2025 the undergr0und",
			image=PyRSS2Gen.Image(
				url="http://hiden.cc/static/icon/misc/hidnet.png",
				title="CrossTalk Video - InDev",
				link="http://crosstalk.im"
			),
			items=rss_items
		)

		rss_xml = rss_feed.to_xml(encoding="utf-8")
		return web.HTTPOk(content_type="text/xml", text=rss_xml)

def _extract_pp_credentials(auth_str: str) -> Optional[Tuple[str, str]]:
	if not auth_str:
		return None
	assert auth_str.startswith(PP)
	auth = {}
	for part in auth_str[len(PP):].split(','):
		parts = part.split('=', 1)
		if len(parts) == 2:
			auth[unquote(parts[0])] = unquote(parts[1])
	email = auth['sign-in']
	pwd = auth['pwd']
	return email, pwd

def _login(req: web.Request, email: str, pwd: str, binary_secret: bool = False, lifetime: int = 30) -> Optional[Tuple[str, datetime, Optional[str]]]:
	backend: Backend = req.app['backend']
	uuid = backend.user_service.login(email, pwd)
	if uuid is None: return None
	bsecret = None
	if binary_secret:
		bsecret = base64.b64encode(secrets.token_bytes(24)).decode('ascii')
	return (*backend.login_auth_service.create_token('ns/login', [uuid, bsecret], lifetime = lifetime), bsecret)

def _contact_is_favorite(user_detail: models.UserDetail, ctc: models.Contact) -> bool:
	groups = user_detail._groups_by_uuid
	for group in ctc._groups.copy():
		if group.id not in groups: continue
		if groups[group.id].is_favorite: return True
	return False
