import datetime
import json
import math
import time
from threading import Thread

import requests

from core.aochat.BaseModule import BaseModule
from core.aochat.server_packets import BuddyAdded
from core.buddy_service import BuddyService
from core.cache_service import CacheService
from core.chat_blob import ChatBlob
from core.command_param_types import Character, Options, Const, Any, NamedParameters
from core.db import DB
from core.decorators import instance, command, event, timerevent
from core.dict_object import DictObject
from core.event_service import EventService
from core.igncore import IgnCore
from core.job_scheduler import JobScheduler
from core.logger import Logger
from core.lookup.org_pork_service import OrgPorkService
from core.lookup.pork_service import PorkService
from core.message_hub_service import MessageHubService
from core.private_channel_service import PrivateChannelService
from core.setting_service import SettingService
from core.setting_types import TextSettingType, ColorSettingType
from core.text import Text
from core.util import Util
from modules.core.accounting.services.account_service import AccountService
from modules.standard.tower.tower_events import TowerEventController


# noinspection DuplicatedCode,SqlCaseVsIf
@instance()
class TrackController(BaseModule):
    threads = {}
    single_org_uri = "https://people.anarchy-online.com/org/stats/d/5/name/%d/basicstats.xml?data_type=json"
    PRIVATE_CHANNEL_PREFIX = "[Priv]"
    PAGE_SIZE = 20
    MESSAGE_SOURCE = "track_log"

    def inject(self, registry):
        self.logger = Logger("TRACK")
        self.bot: IgnCore = registry.get_instance("bot")
        self.util: Util = registry.get_instance("util")
        self.pork: PorkService = registry.get_instance("pork_service")
        self.job_scheduler: JobScheduler = registry.get_instance("job_scheduler")
        self.text: Text = registry.get_instance("text")
        self.event_service: EventService = registry.get_instance("event_service")
        self.setting_service: SettingService = registry.get_instance("setting_service")
        self.buddy_service: BuddyService = registry.get_instance("buddy_service")
        self.account_service: AccountService = registry.get_instance("account_service")
        self.db: DB = registry.get_instance("db")
        self.priv: PrivateChannelService = registry.get_instance("private_channel_service")
        self.tower: TowerEventController = registry.get_instance("tower_controller")
        self.message_hub_service: MessageHubService = registry.get_instance("message_hub_service")
        self.org_pork: OrgPorkService = registry.get_instance("org_pork_service")
        self.cache: CacheService = registry.get_instance("cache_service")
        self.relay_hub_service: MessageHubService = registry.get_instance("message_hub_service")


    def pre_start(self):
        self.event_service.register_event_type("track_logon")
        self.event_service.register_event_type("track_logoff")
        self.setting_service.register(self.module_name, "track_on_color", "#FF0000", ColorSettingType(),
                                      "Color for Track logon")
        self.setting_service.register(self.module_name, "track_off_color", "#00FF00", ColorSettingType(),
                                      "Color for Track logoff")
        self.setting_service.register(self.module_name, "autotrack", 'none',
                                      TextSettingType(['omni', 'clan', 'neutral', "none"]),
                                      "Autotrack all players initiating tower attacks towards this faction")
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS track("
            "char_id int not null primary key, "
            "initiator int not null, "
            "reason varchar(255))")
        self.db.exec("CREATE TABLE IF NOT EXISTS track_orgs(org_id int not null primary key, initiator int not null)")
        self.db.exec("CREATE TABLE IF NOT EXISTS track_org_members(char_id int not null, org_id int not null)")
        self.message_hub_service.register_message_source(self.MESSAGE_SOURCE)

    @event(event_type="connect", description="Autoadd tracked players on connect", is_hidden=True)
    def connect_add(self, _1, _2):
        for user in self.db.query("SELECT * from track"):
            self.buddy_service.add_buddy(user.char_id, "track")
        for user in self.db.query("SELECT char_id FROM track_org_members"):
            self.buddy_service.add_buddy(user.char_id, "track")

    @event(event_type="buddy_logon", description="Fire tracker events", is_hidden=True)
    def track_fire_logon(self, _, event_data):
        if self.bot.is_ready():
            if "track" in (self.buddy_service.get_buddy(event_data.char_id) or {'types': []})["types"]:
                print("Login: ", event_data.char_id)
                if type(event_data) == BuddyAdded:
                    self.event_service.fire_event("track_logon",
                                                  self.db.query_single("SELECT * from player where char_id=?",
                                                                       [event_data.char_id]))
                self.db.exec("INSERT IGNORE INTO online VALUES(?, ?, ?)",
                             [event_data.char_id, 'track', self.bot.get_char_id()])
        else:
            self.job_scheduler.delayed_job(self.track_fire_logon, 10,
                                           DictObject({'char_id': event_data.char_id, 'repeat': True}))

    @event(event_type="buddy_logoff", description="Fire tracker events", is_hidden=True)
    def track_fire_logoff(self, _1, event_data):
        if buddy := self.buddy_service.get_buddy(event_data.char_id):
            if "track" in buddy["types"]:
                self.event_service.fire_event("track_logoff",
                                              self.db.query_single("SELECT * from player where char_id=?",
                                                                   [event_data.char_id]))
                self.db.exec("DELETE FROM online where char_id=? and bot=?",
                             [event_data.char_id, self.bot.get_char_id()])

    @event(event_type="track_logon", description="Fire tracker logon events")
    def track_logon(self, _1, user):
        if self.bot.is_ready():
            color = self.setting_service.get("track_on_color").format_text("ON")
            self.send_t_warn(0, f'{color} :: {self.text.format_char_info(user)}')

    @event(event_type="track_logoff", description="Fire tracker logoff events")
    def track_logoff(self, _1, user):
        if self.bot.is_ready():
            color = self.setting_service.get("track_off_color").format_text("OFF")
            self.send_t_warn(0, f'{color} :: {self.text.format_char_info(user)}')

    @event(event_type=TowerEventController.TOWER_ATTACK_EVENT, description="Autottrack players attacking our faction")
    def tower_attack_event(self, _, event_data):
        attacker = event_data.attacker
        if event_data.defender.faction.lower() == self.setting_service.get_value("autotrack"):
            if not self.buddy_service.get_buddy(attacker.char_id):
                if self.buddy_service.add_buddy(attacker.char_id, "track"):
                    self.send_t_warn(0,
                                     f"Now tracking: "
                                     f"<{attacker.faction.lower()}>{attacker.name}</{attacker.faction.lower()}>")
                    self.db.exec("INSERT IGNORE INTO track VALUES(?, ?, ?)",
                                 [attacker.char_id, self.bot.get_char_id(), "attacked us"])

    def send_t_warn(self, _, msg):
        self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, "[T] " + msg, "[<cyan>T</cyan>] " + msg)

    def get_tracked(self, order, online):
        where = "where o.char_id IS NOT NULL" if online else ""
        return self.db.query(f"SELECT p.name, p.level, p.ai_level, p.org_name, p.profession, p.faction, "
                             f"p2.name as initiator, "
                             f"CASE WHEN o.char_id IS NOT NULL THEN 1 ELSE 0 END AS online, "
                             f"t.reason from track t "
                             f"left join player p on p.char_id = t.char_id "
                             f"left join player p2 on p2.char_id = t.initiator "
                             f"left join online o on o.char_id = t.char_id {where} group by t.char_id order by {order}")

    @command(command="track", params=[Const('add'), Character("character"), Any('reason')], access_level="leader",
             description="Initiate tracking for the character", sub_command="add")
    def track_add(self, request, _, user, reason):
        if not user.char_id:
            return f"Character <highlight>{user.name}</highlight> not found."
        self.pork.load_character_info(user.char_id, user.name)
        if acc := self.account_service.get_account(request.sender.char_id):
            if "track" in (self.buddy_service.get_buddy(user.char_id) or {'types': []})["types"]:
                return f"Character <highlight>{user.name}</highlight> is already being tracked."
            else:
                self.pork.load_character_info(user.char_id, user.name)
                self.send_t_warn(0, f'Tracking initiated: {user.name} by {acc.name}')
                self.buddy_service.add_buddy(user.char_id, 'track')
                self.db.exec("INSERT IGNORE INTO track VALUES(?, ?, ?)",
                             [user.char_id, acc.main, reason])

    @command(command="orgtrack", params=[Const('add'), Any("Organisation")], access_level="moderator",
             description="Initiate tracking for an Organisation", sub_command="add")
    def track_addorg(self, request, _, search):
        orgs = self.org_pork.find_org(search)
        if len(orgs) == 1:
            orgs = orgs[0]
            if self.db.exec("REPLACE INTO track_orgs(org_id, initiator) VALUES(?, ?)",
                            [orgs.org_id, request.sender.char_id]) == 1:
                request.reply(f"Adding the organisation <highlight>{orgs.org_name}</highlight> to the tracking...")
                org_adder = Thread(name=orgs.org_id, target=self.fetch_single,
                                   args=(orgs.org_id, orgs.org_name, request))
                self.threads[orgs.org_id] = org_adder
                org_adder.start()
            else:
                return f"The organisation <highlight>{orgs.org_name}</highlight> is already being tracked."
        elif len(orgs) == 0:
            return f"No org with the name <highlight>{search}</highlight> was found on PoRK."
        else:
            blob = "Your search had multiple results; please pick an org:<br>"
            for org in orgs:
                blob += f'[{self.text.make_chatcmd("Add", f"/tell <myname> orgtrack add {org.org_id}")}]' \
                        f'[{self.text.make_chatcmd("More", f"/tell <myname> org info {org.org_id}")}]' \
                        f' <highlight>{org.org_name}</highlight> (<highlight>{org.org_id}</highlight>) ' \
                        f'<{org.faction.lower()}>{org.faction}</{org.faction.lower()}> [<highlight>{org.member_count}</highlight> members]' \
                        f'<br><pagebreak>'
            return ChatBlob("Pick an Org", blob)

    @command(command="orgtrack", params=[Const("rem"), Any("Organisation")], access_level="moderator",
             description="Add an organisation to the tracking", sub_command="rem")
    def orgtrack_rem(self, request, _, search):
        orgs = self.org_pork.find_org(search)
        orgs = [x for x in orgs if x.org_name.lower().startswith(search.lower()) or (x.org_id == int(search) if search.isdigit() else False)]
        if len(orgs) == 1:
            orgs = orgs[0]
            if self.db.exec("DELETE FROM track_orgs where org_id = ?", [orgs.org_id]) == 1:
                org_remover = Thread(name=orgs.org_id, target=self.remove_single, args=(orgs.org_id, orgs.org_name))
                self.threads[orgs.org_id] = org_remover
                org_remover.start()
                return f"Removed the organisation <highlight>{orgs.org_name}</highlight> from the roster."
            else:
                return f"The organisation <highlight>{orgs.org_name}</highlight> is not on the roster list."
        elif len(orgs) == 0:
            return f"The organisation <highlight>{search}</highlight> is not on the roster list."
        else:
            blob = "Your search had multiple results; please pick an org:<br>"
            for org in orgs:
                blob += f'[{self.text.make_chatcmd("Remove", f"/tell <myname> orgtrack rem {org.org_id}")}]' \
                        f'[{self.text.make_chatcmd("More", f"/tell <myname> org info {org.org_id}")}]' \
                        f' <highlight>{org.org_name}</highlight> (<highlight>{org.org_id}</highlight>) ' \
                        f'<{org.faction.lower()}>{org.faction}</{org.faction.lower()}> [<highlight>{org.member_count}</highlight> members]' \
                        f'<br><pagebreak>'
            return ChatBlob("Pick an Org", blob)

    @command(command="track", params=[Const('rem'), Character("character")], access_level="moderator",
             description="Remove character from the tracking", sub_command="rem")
    def track_rem(self, request, _, user):
        if not user.char_id:
            return f"Character <highlight>{user.name}</highlight> not found."
        if acc := self.account_service.get_account(request.sender.char_id):
            if self.buddy_service.remove_buddy(user.char_id, "track"):
                self.db.exec("DELETE FROM track where char_id=?", [user.char_id])
                self.send_t_warn(0, f'Tracking stopped: {user.name} by {acc.name}')
                return f"Character <highlight>{user.name}</highlight> is nolonger being tracked."
            else:
                return f"Character <highlight>{user.name}</highlight> is not being tracked."

    @command(command="track",
             params=[Const('list', is_optional=True), Options(["org", "prof", "tl"], is_optional=True),
                     NamedParameters(["page"])],
             access_level="member",
             description="Shows tracked players")
    def track_list(self, _, const, group, named_params):
        page = int(named_params.page or "1")
        offset = (page - 1) * self.PAGE_SIZE
        if group is None:
            group = 'tl'
        if group == "org":
            players = self.get_tracked('p.org_name, p.profession, p.level desc, p.name', False if const else True)
            return self.format_page(players, "org", offset, page, f"Tracklist by Organisation ({len(players)})",
                                    'No tracked users online', f"track {const or ''} {group or ''}")

        elif group == "prof":
            players = self.get_tracked('p.profession, p.level desc, p.name', False if const else True)
            return self.format_page(players, "prof", offset, page, f"Tracklist by Profession ({len(players)})",
                                    'No tracked users online', f"track {const or ''} {group or ''}")
        elif group == "tl":
            players = self.get_tracked('p.level desc, p.profession, p.name', False if const else True)
            return self.format_page(players, "tl", offset, page, f"Tracklist by Titlelevel ({len(players)})",
                                    'No tracked users online', f"track {const or ''} {group or ''}")
    @command(command="orgtrack",
             params=[Const('info')],
             access_level="member",
             description="Shows tracked orgs")
    def orgtrack_info(self, _, const):
        blob = ""
        for org in self.db.query("SELECT a.*, o.* from track_orgs o LEFT JOIN all_orgs a on o.org_id = a.org_id"):
            blob += f"- [{self.text.make_tellcmd('Info', f'orgs info {org.org_id}')}] " \
                    f"[{self.text.make_tellcmd('Remove', f'orgtrack rem {org.org_id}')}]" \
                    f" {self.text.get_formatted_faction(org.faction, org.org_name)} ({org.org_id}) with <highlight>{org.member_count}</highlight> memebrs"
        return ChatBlob("Currently Tracked Organisations", blob)
    @command(command="orgtrack",
             params=[Const('list', is_optional=True), NamedParameters(["page"])],
             access_level="member",
             description="Shows tracked orgmembers")
    def track_listorg(self, _, const, named_params):

        page = int(named_params.page or "1")
        offset = (page - 1) * self.PAGE_SIZE
        where = "where o.char_id IS NOT NULL" if not const else ""
        players = self.db.query(f"SELECT p.name, p.level, p.ai_level, p.org_name, p.profession, p.faction, "
                                f"CASE WHEN o.char_id IS NOT NULL THEN 1 ELSE 0 END AS online from track_org_members t "
                                f"left join player p on p.char_id = t.char_id "
                                f"left join online o on o.char_id = t.char_id {where} group by t.char_id order by p.org_name, p.profession, p.level desc, p.name")

        return self.format_page_org(players, "org", offset, page, f"Tracklist by Organisation ({len(players)})",
                                    'No tracked users online', f"orgtrack {const or ''} ")

    def format_row(self, user):
        org = f"[<{user.faction.lower()}>{user.org_name}</{user.faction.lower()}>] " if user.org_name else ""
        return f"<tab>{self.util.get_prof_icon(user.profession)} " \
               f"{self.text.zfill(user.level, 220)}:<green>{self.text.zfill(user.ai_level, 220)}</green> " \
               f"<{user.faction.lower()}>{user.name}</{user.faction.lower()}>" \
               f" {org} {'[<red>ONLINE</red>]' if user.online == 1 else ''}\n"

    def format_page(self, tracked, order, offset, page, title, nullmsg, cmd):
        selected = tracked[offset:offset + self.PAGE_SIZE]
        count = len(selected)
        pages = ""
        if page > 1:
            pages += "Pages:   " + self.text.make_tellcmd("«« Page %d" % (page - 1), f'{cmd} --page={page - 1}')
        if offset + self.PAGE_SIZE < len(tracked):
            pages += f"   Page {page}/{math.ceil(len(tracked) / self.PAGE_SIZE)}"
            pages += "   " + self.text.make_tellcmd("Page %d »»" % (page + 1), f'{cmd} --page={page + 1}')
        pages += "\n"
        if count == 0:
            return nullmsg
        else:
            blob = "\n\n" + pages + "<font color=CCInfoText>"
            if order == "prof":
                prof = ""
                for player in selected:
                    if player.profession != prof:
                        prof = player.profession
                        blob += f"\n<pagebreak><header>{player.profession}</header>\n"
                    blob += self.format_row(player)
            elif order == "tl":
                tl = ""
                for player in selected:
                    titlelevel = self.util.get_title_level(player.level)
                    if titlelevel != tl:
                        tl = titlelevel
                        blob += f"\n<pagebreak><header>TL{titlelevel}</header>\n"
                    blob += self.format_row(player)
            elif order == "org":
                org_name = ""
                for player in selected:
                    if player.org_name != org_name:
                        org_name = player.org_name
                        blob += f"\n<pagebreak><header>{player.org_name}</header>\n"
                    blob += self.format_row(player)

            blob += "</font>\n" + pages
            return ChatBlob(title, blob)

    def format_page_org(self, tracked, order, offset, page, title, nullmsg, cmd):
        selected = tracked[offset:offset + self.PAGE_SIZE]
        count = len(selected)
        pages = ""
        if page > 1:
            pages += "Pages:   " + self.text.make_tellcmd("«« Page %d" % (page - 1), f'{cmd} --page={page - 1}')
        if offset + self.PAGE_SIZE < len(tracked):
            pages += f"   Page {page}/{math.ceil(len(tracked) / self.PAGE_SIZE)}"
            pages += "   " + self.text.make_tellcmd("Page %d »»" % (page + 1), f'{cmd} --page={page + 1}')
        pages += "\n"
        if count == 0:
            return nullmsg
        else:
            blob = "\n\n" + pages + "<font color=CCInfoText>"
            if order == "org":
                org_name = ""
                for player in selected:
                    if player.org_name != org_name:
                        org_name = player.org_name
                        blob += f"\n<pagebreak><header>{player.org_name}</header>\n"
                    blob += self.format_row(player)

            blob += "</font>\n" + pages
            return ChatBlob(title, blob)

    def format_row_org(self, user):
        org = f"[<{user.faction.lower()}>{user.org_name}</{user.faction.lower()}>] " if user.org_name else ""
        return f"<tab>{self.util.get_prof_icon(user.profession)} " \
               f"{self.text.zfill(user.level, 220)}:<green>{self.text.zfill(user.ai_level, 220)}</green> " \
               f"<{user.faction.lower()}>{user.name}</{user.faction.lower()}>" \
               f" {org}init by <highlight>{user.initiator}</highlight>: {user.reason} " \
               f"{'[<red>ONLINE</red>]' if user.online == 1 else ''}\n"

    @timerevent(budatime="24h", description="Update Tracked Orgs")
    def fetch_orgs(self, _, _1):
        def discover():
            start = time.time()
            self.logger.info("Fetching orgdata..")
            output = []
            data = []
            data2 = []
            timestamp = 0
            ours = self.db.query("SELECT * FROM track_orgs t LEFT JOIN all_orgs a ON t.org_id = a.org_id")
            for org in ours:
                result = requests.get(self.single_org_uri % org.org_id).json()
                if result and len(result[1]) > 0:
                    self.cache.store('org_roster', f"{org.org_id}.5.json", json.dumps(result))
                else:
                    result = json.loads(self.cache.retrieve('org_roster', f"{org.org_id}.5.json").data)
                d = datetime.datetime.strptime(result[2] + " +0000", '%Y/%m/%d %H:%M:%S %z').timestamp()
                if d > timestamp:
                    timestamp = d

                for char_info in result[1]:
                    data.append((char_info["CHAR_INSTANCE"], char_info["NAME"], char_info["FIRSTNAME"],
                                 char_info["LASTNAME"], char_info["LEVELX"], char_info["BREED"],
                                 char_info["SEX"], result[0]["SIDE_NAME"], char_info["PROF"],
                                 char_info["PROF_TITLE"], char_info["DEFENDER_RANK_TITLE"], char_info["ALIENLEVEL"],
                                 result[0]["ORG_INSTANCE"], result[0]["NAME"], char_info["RANK_TITLE"],
                                 char_info["RANK"], char_info["CHAR_DIMENSION"], char_info["HEADID"],
                                 0, char_info["PVPTITLE"] or "", "roster", int(time.time())))
                    data2.append((char_info['CHAR_INSTANCE'], result[0]['ORG_INSTANCE']))

                    if not self.buddy_service.get_buddy(char_info["CHAR_INSTANCE"]):
                        self.buddy_service.add_buddy(char_info["CHAR_INSTANCE"], "track")
                        output.append(DictObject({"action": "JOIN",
                                                  "name": char_info['NAME'],
                                                  "org_name": result[0]["NAME"],
                                                  "org_id": result[0]["ORG_INSTANCE"],
                                                  "level": char_info["LEVELX"],
                                                  "ai_level": char_info["ALIENLEVEL"],
                                                  "ranks": 0}))
                self.logger.info(f"[TRACK] Organisation {org.org_name} has been updated.")
            if len(data) > 1:
                with self.db.lock:
                    with self.db.pool.get_connection() as conn:
                        with conn.cursor() as cur:
                            cur.executemany("INSERT INTO player(char_id, name, first_name, last_name, "
                                            "level, breed, gender, faction, profession, profession_title, "
                                            "ai_rank, ai_level, org_id, org_name, org_rank_name, "
                                            "org_rank_id, dimension, head_id, pvp_rating, pvp_title, "
                                            "source, last_updated) VALUES "
                                            "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
                                            "ON DUPLICATE KEY UPDATE first_name=VALUE(first_name), "
                                            "last_name=VALUE(last_name), level=VALUE(level), "
                                            "breed=VALUE(breed), gender=VALUE(gender), "
                                            "faction=VALUE(faction), profession=VALUE(profession), "
                                            "profession_title=VALUE(profession_title),ai_rank=VALUE(ai_rank), "
                                            "ai_level=VALUE(ai_level), org_name=VALUE(org_name), "
                                            "org_id=VALUE(org_id), org_rank_name=VALUE(org_rank_name), "
                                            "org_rank_id=VALUE(org_rank_id), source=VALUE(source), "
                                            "last_updated=VALUE(last_updated)".replace("?", "%s"), data)
                            cur.executemany("INSERT INTO track_org_members (char_id, org_id) VALUES(?,?)".replace("?", "%s"), data2)
                            conn.commit()
            for org in ours:
                left = self.db.query("SELECT p.* FROM track_org_members t LEFT JOIN player p on t.char_id = p.char_id where t.org_id=? and (p.last_updated < ? OR t.org_id != p.org_id)", [org.org_id, time.time()-25*60*60, org.org_id])
                for player in left:
                    bonus = None
                    player = DictObject(player)
                    if self.buddy_service.remove_buddy(player.char_id, "track"):
                        bonus = "LEAVE"
                    new_data = self.pork.request_char_info(player.name, player.dimension)
                    if new_data and new_data.char_id == player.char_id:
                        self.pork.save_character_info(new_data)
                    else:
                        bonus = "DEL"
                    if bonus:
                        output.append(DictObject({"action": bonus,
                                                  "name": player.name,
                                                  "org_name": player.org_name,
                                                  "org_id": player.org_id,
                                                  "level": player.level,
                                                  "ai_level": player.ai_level}))

                self.db.exec("DELETE FROM track_org_members where char_id not in (SELECT char_id from player where org_id=?)", [org.org_id])

            self.log(output, time.time() - start)
            self.logger.info(
                f"Successfully fetched {len(data)} players from {len(ours)} orgs "
                f"in {time.time() - start:.2f} seconds. - ")
            while (timestamp + 24 * 60 * 60) < datetime.datetime.now().timestamp():
                timestamp += 24 * 60 * 60
            self.db.exec("UPDATE timer_event SET next_run=? WHERE handler=? AND event_sub_type=?", [
                int(timestamp +24*60*60 + 10*60),
                "modules.standard.track.track_controller.TrackController.fetch_orgs",
                86400
            ])
            del self.threads['roster']

        if "roster" not in self.threads.keys():
            thread = Thread(name="roster", target=discover, daemon=True)
            self.threads["roster"] = thread
            thread.start()

    def fetch_single(self, org_id, org_name, sender: object):
        start = time.time()
        data = []
        data2 = []
        accounts = []
        self.logger.info("Fetching orgdata..")
        count = 0
        result = requests.get(self.single_org_uri % org_id).json()
        for char_info in result[1]:
            data.append((char_info["CHAR_INSTANCE"], char_info["NAME"], char_info["FIRSTNAME"],
                         char_info["LASTNAME"],
                         char_info["LEVELX"], char_info["BREED"],
                         char_info["SEX"], result[0]["SIDE_NAME"], char_info["PROF"],
                         char_info["PROF_TITLE"], char_info["DEFENDER_RANK_TITLE"], char_info["ALIENLEVEL"],
                         result[0]["ORG_INSTANCE"], result[0]["NAME"], char_info["RANK_TITLE"],
                         char_info["RANK"], char_info["CHAR_DIMENSION"], char_info["HEADID"],
                         0, char_info["PVPTITLE"] or "", "roster", int(time.time())))
            data2.append((char_info['CHAR_INSTANCE'], result[0]['ORG_INSTANCE']))

            accounts.append((char_info["CHAR_INSTANCE"], char_info["CHAR_INSTANCE"], result[0]["ORG_INSTANCE"],
                             start, start))
            self.buddy_service.add_buddy(char_info['CHAR_INSTANCE'], "track")
            count += 1
        with self.db.pool.get_connection() as conn:
            with conn.cursor() as cur:
                cur.executemany("REPLACE INTO player(char_id, name, first_name, last_name, level, breed, "
                                "gender, faction, profession, profession_title, ai_rank, ai_level, "
                                "org_id, org_name, org_rank_name, org_rank_id, dimension, head_id, "
                                "pvp_rating, pvp_title, source, last_updated) VALUES "
                                "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)".replace("?", "%s"), data)
                cur.executemany("REPLACE INTO track_org_members(char_id, org_id) VALUES (?, ?)".replace("?", "%s"), data2)

        self.logger.info(f"Organisation {org_name} is now being tracked!")
        sender.reply(f"<highlight>{org_name}</highlight> is now being tracked. "
                     f"Runtime: {time.time() - start:.2f} seconds.")

        self.logger.info(f"Successfully fetched {count} players in {time.time() - start} seconds.")
        del self.threads[org_id]

    def remove_single(self, org_id, org_name):
        members = self.db.query("SELECT * from track_org_members where org_id=?", [org_id])
        for member in members:
            self.buddy_service.remove_buddy(member.char_id, "member")
        self.db.exec("DELETE FROM track_org_members where org_id=?", [org_id])
        self.db.exec("DELETE FROM online where char_id in (SELECT char_id from player where org_id=?)", [org_id])
        self.logger.info(f"Organisation {org_name} removed!")
        del self.threads[org_id]

    def log(self, blob, duration):
        out = []
        s = []
        current = ""

        for entry in blob:
            s.append(f"[{entry.org_name}] [{entry.action}] {entry.name} "
                     f"({entry.level}/{entry.ai_level})\n")
        s = sorted(s)
        if len(s) > 0:
            s.append(f"\nRuntime: {duration:.2f} seconds.")
        for entry in s:
            if len(current) > 1500:
                out.append(current)
                current = ""
            current += entry
        if len(current) > 10:
            out.append(current)
        if len(out) > 0:
            y = 0
            for x in out:
                y += 1
                blob = ChatBlob(f"Recent changes - {datetime.date.today()} - ({y}/{len(out)})", x, embed=True)
                self.relay_hub_service.send_message("member_logger", None, blob, blob)