import datetime
import hashlib
import threading
from decimal import Decimal
from typing import Any, Dict, List, NamedTuple, Optional, Tuple, Union

from requests import Response, Session, post

from erepublik import utils
from erepublik.utils import json

INDUSTRIES = {1: "Food", 2: "Weapons", 4: "House", 23: "Aircraft",
              7: "FRM q1", 8: "FRM q2", 9: "FRM q3", 10: "FRM q4", 11: "FRM q5",
              12: "WRM q1", 13: "WRM q2", 14: "WRM q3", 15: "WRM q4", 16: "WRM q5",
              18: "HRM q1", 19: "HRM q2", 20: "HRM q3", 21: "HRM q4", 22: "HRM q5",
              24: "ARM q1", 25: "ARM q2", 26: "ARM q3", 27: "ARM q4", 28: "ARM q5", }


class Country:
    id: int
    name: str
    link: str
    iso: str

    def __init__(self, country_id: int, name: str, link: str, iso: str):
        self.id = country_id
        self.name = name
        self.link = link
        self.iso = iso

    def __repr__(self):
        return f"Country({self.id}, '{self.name}', '{self.link}', '{self.iso}')"

    def __str__(self):
        return f"#{self.id} {self.name}"

    def __format__(self, format_spec):
        return self.iso

    def __int__(self):
        return self.id

    def __eq__(self, other):
        if isinstance(other, (int, float)):
            return self.id == int(other)
        else:
            try:
                return self.id == int(other)
            except ValueError:
                return self == other

    @property
    def __dict__(self):
        return dict(id=self.id, name=self.name, iso=self.iso)


class ErepublikException(Exception):
    def __init__(self, message):
        super().__init__(message)


class ErepublikNetworkException(ErepublikException):
    def __init__(self, message, request):
        super().__init__(message)
        self.request = request


class Holding:
    id: int
    region: int
    companies: List["Company"]

    def __init__(self, _id: int, region: int, citizen):
        self.citizen = citizen
        self.id: int = _id
        self.region: int = region
        self.companies: List["Company"] = list()

    @property
    def wam_count(self) -> int:
        return sum([company.wam_enabled and not company.already_worked for company in self.companies])

    @property
    def wam_companies(self) -> List["Company"]:
        return [company for company in self.companies if company.wam_enabled]

    @property
    def employable_companies(self) -> List["Company"]:
        return [company for company in self.companies if company.preset_works]

    def add_company(self, company: "Company"):
        self.companies.append(company)
        self.companies.sort()

    def get_wam_raw_usage(self) -> Dict[str, Decimal]:
        frm = Decimal("0.00")
        wrm = Decimal("0.00")
        for company in self.wam_companies:
            if company.industry in [1, 7, 8, 9, 10, 11]:
                frm += company.raw_usage
            elif company.industry in [2, 12, 13, 14, 15, 16]:
                wrm += company.raw_usage
        return dict(frm=frm, wrm=wrm)

    def get_wam_companies(self, raw_factory: bool = None):
        raw = []
        factory = []
        for company in self.wam_companies:
            if not company.already_worked and not company.cannot_wam_reason == "war":
                if company.is_raw:
                    raw.append(company)
                else:
                    factory.append(company)
        if raw_factory is not None and not raw_factory:
            return factory
        elif raw_factory is not None and raw_factory:
            return raw
        elif raw_factory is None:
            return raw + factory
        else:
            raise ErepublikException("raw_factory should be True/False/None")

    def __str__(self):
        name = f"Holding (#{self.id}) with {len(self.companies)} "
        if len(self.companies) % 10 == 1:
            name += "company"
        else:
            name += "companies"
        return name

    def __repr__(self):
        return str(self)

    @property
    def __dict__(self):
        return dict(name=str(self), id=self.id, region=self.region, companies=self.companies, wam_count=self.wam_count)


class Company:
    holding: Holding
    id: int
    quality: int
    is_raw: bool
    raw_usage: Decimal
    products_made: Decimal
    wam_enabled: bool
    can_wam: bool
    cannot_wam_reason: str
    industry: int
    already_worked: bool
    preset_works: int

    def __init__(
        self, holding: Holding, _id: int, quality: int, is_raw: bool, effective_bonus: Decimal, raw_usage: Decimal,
        base_production: Decimal, wam_enabled: bool, can_wam: bool, cannot_wam_reason: str, industry: int,
        already_worked: bool, preset_works: int
    ):
        self.holding: Holding = holding
        self.id: int = _id
        self.industry: int = industry
        self.quality: int = self._get_real_quality(quality)
        self.is_raw: bool = is_raw
        self.wam_enabled: bool = wam_enabled
        self.can_wam: bool = can_wam
        self.cannot_wam_reason: str = cannot_wam_reason
        self.already_worked: bool = already_worked
        self.preset_works: int = preset_works

        self.products_made = self.raw_usage = Decimal(base_production) * Decimal(effective_bonus)
        if not self.is_raw:
            self.raw_usage = - self.products_made * raw_usage

    def _get_real_quality(self, quality) -> int:
        #  7: "FRM q1",  8: "FRM q2",  9: "FRM q3", 10: "FRM q4", 11: "FRM q5",
        # 12: "WRM q1", 13: "WRM q2", 14: "WRM q3", 15: "WRM q4", 16: "WRM q5",
        # 18: "HRM q1", 19: "HRM q2", 20: "HRM q3", 21: "HRM q4", 22: "HRM q5",
        # 24: "ARM q1", 25: "ARM q2", 26: "ARM q3", 27: "ARM q4", 28: "ARM q5",
        if 7 <= self.industry <= 11:
            return self.industry % 6
        elif 12 <= self.industry <= 16:
            return self.industry % 11
        elif 18 <= self.industry <= 22:
            return self.industry % 17
        elif 24 <= self.industry <= 28:
            return self.industry % 23
        else:
            return quality

    @property
    def _internal_industry(self) -> int:
        #  7: "FRM q1",  8: "FRM q2",  9: "FRM q3", 10: "FRM q4", 11: "FRM q5",
        # 12: "WRM q1", 13: "WRM q2", 14: "WRM q3", 15: "WRM q4", 16: "WRM q5",
        # 18: "HRM q1", 19: "HRM q2", 20: "HRM q3", 21: "HRM q4", 22: "HRM q5",
        # 24: "ARM q1", 25: "ARM q2", 26: "ARM q3", 27: "ARM q4", 28: "ARM q5",
        if 7 <= self.industry <= 11:
            return 7
        elif 12 <= self.industry <= 16:
            return 12
        elif 18 <= self.industry <= 22:
            return 18
        elif 24 <= self.industry <= 28:
            return 24
        else:
            return self.industry

    @property
    def _sort_keys(self):
        return not self.is_raw, self._internal_industry, -self.quality, self.id

    def __hash__(self):
        return hash(self._sort_keys)

    def __lt__(self, other: "Company"):
        return self._sort_keys < other._sort_keys

    def __le__(self, other: "Company"):
        return self._sort_keys <= other._sort_keys

    def __gt__(self, other: "Company"):
        return self._sort_keys > other._sort_keys

    def __ge__(self, other: "Company"):
        return self._sort_keys >= other._sort_keys

    def __eq__(self, other: "Company"):
        return self._sort_keys == other._sort_keys

    def __ne__(self, other: "Company"):
        return self._sort_keys != other._sort_keys

    def __str__(self):
        name = f"(#{self.id:>9d}) {INDUSTRIES[self.industry]}"
        if not self.is_raw:
            name += f" q{self.quality}"
        return name

    def __repr__(self):
        return str(self)

    @property
    def __dict__(self):
        return dict(name=str(self), holding=self.holding.id, id=self.id, quality=self.quality, is_raw=self.is_raw,
                    raw_usage=self.raw_usage, products_made=self.products_made, wam_enabled=self.wam_enabled,
                    can_wam=self.can_wam, cannot_wam_reason=self.cannot_wam_reason, industry=self.industry,
                    already_worked=self.already_worked, preset_works=self.preset_works)

    def dissolve(self) -> Response:
        self.holding.citizen.write_log(f"{self} dissolved!")
        # noinspection PyProtectedMember
        return self.holding.citizen._post_economy_sell_company(self.id, self.holding.citizen.details.pin, sell=False)

    def upgrade(self, level: int) -> Response:
        # noinspection PyProtectedMember
        return self.holding.citizen._post_economy_upgrade_company(self.id, level, self.holding.citizen.details.pin)


class MyCompanies:
    work_units: int = 0
    next_ot_time: datetime.datetime
    ff_lockdown: int = 0

    holdings: Dict[int, Holding]
    companies: List[Company]

    def __init__(self, citizen):
        from erepublik import Citizen
        self.citizen: Citizen = citizen
        self.holdings: Dict[int, Holding] = dict()
        self.companies: List[Company] = list()
        self.next_ot_time = utils.now()

    def prepare_holdings(self, holdings: Dict[str, Dict[str, Any]]):
        """
        :param holdings: Parsed JSON to dict from en/economy/myCompanies
        """
        for holding in holdings.values():
            if holding.get('id') not in self.holdings:
                self.holdings.update(
                    {int(holding.get('id')): Holding(holding['id'], holding['region_id'], self.citizen)})
        if not self.holdings.get(0):
            self.holdings.update({0: Holding(0, 0, self.citizen)})  # unassigned

    def prepare_companies(self, companies: Dict[str, Dict[str, Any]]):
        """
        :param companies: Parsed JSON to dict from en/economy/myCompanies
        """
        self.__clear_data()
        for company_dict in companies.values():
            holding = self.holdings.get(int(company_dict['holding_company_id']))
            quality = company_dict.get('quality')
            is_raw = company_dict.get('is_raw')
            if is_raw:
                raw_usage = Decimal('0.0')
            else:
                raw_usage = Decimal(str(company_dict.get('upgrades').get(str(quality)).get('raw_usage')))
            company = Company(
                holding, company_dict.get('id'), quality, is_raw,
                Decimal(str(company_dict.get('effective_bonus'))) / 100,
                raw_usage, Decimal(str(company_dict.get('base_production'))), company_dict.get('wam_enabled'),
                company_dict.get('can_work_as_manager'), company_dict.get('cannot_work_as_manager_reason'),
                company_dict.get('industry_id'), company_dict.get('already_worked'), company_dict.get('preset_works')
            )
            self.companies.append(company)
            holding.add_company(company)

    def get_employable_factories(self) -> Dict[int, int]:
        return {company.id: company.preset_works for company in self.companies if company.preset_works}

    def get_total_wam_count(self) -> int:
        return sum([holding.wam_count for holding in self.holdings.values()])

    @staticmethod
    def get_needed_inventory_usage(companies: Union[Company, List[Company]]) -> Decimal:
        if isinstance(companies, list):
            return sum([company.products_made * 100 if company.is_raw else 1 for company in companies])
        else:
            return companies.products_made

    def __str__(self):
        return f"MyCompanies: {len(self.companies)} companies in {len(self.holdings)} holdings"

    def __repr__(self):
        return str(self)

    def __clear_data(self):
        for holding in self.holdings.values():
            holding.companies.clear()
        self.companies.clear()

    @property
    def __dict__(self):
        return dict(name=str(self), work_units=self.work_units, next_ot_time=self.next_ot_time,
                    ff_lockdown=self.ff_lockdown, holdings=self.holdings, company_count=len(self.companies))


class Config:
    email = ""
    password = ""
    work = True
    train = True
    wam = False
    ot = True
    auto_sell: List[str] = None
    auto_sell_all = False
    employees = False
    fight = False
    air = False
    ground = False
    all_in = False
    next_energy = False
    boosters = False
    travel_to_fight = False
    always_travel = False
    epic_hunt = False
    epic_hunt_ebs = False
    rw_def_side = False
    interactive = True
    continuous_fighting = False
    auto_buy_raw = False
    force_wam = False
    sort_battles_time = True
    force_travel = False
    telegram = True
    telegram_chat_id = 0
    telegram_token = ""

    def __init__(self):
        self.auto_sell = []

    def reset(self):
        self.work = True
        self.train = True
        self.wam = False
        self.ot = True
        self.auto_sell = list()
        self.auto_sell_all = False
        self.employees = False
        self.fight = False
        self.air = False
        self.ground = False
        self.all_in = False
        self.next_energy = False
        self.boosters = False
        self.travel_to_fight = False
        self.always_travel = False
        self.epic_hunt = False
        self.epic_hunt_ebs = False
        self.rw_def_side = False
        self.interactive = True
        self.continuous_fighting = False
        self.auto_buy_raw = False
        self.force_wam = False
        self.sort_battles_time = True
        self.force_travel = False
        self.telegram = True
        self.telegram_chat_id = 0
        self.telegram_token = ""

    @property
    def __dict__(self):
        return dict(email=self.email, work=self.work, train=self.train, wam=self.wam, ot=self.ot,
                    auto_sell=self.auto_sell, auto_sell_all=self.auto_sell_all, employees=self.employees,
                    fight=self.fight, air=self.air, ground=self.ground, all_in=self.all_in,
                    next_energy=self.next_energy, boosters=self.boosters, travel_to_fight=self.travel_to_fight,
                    always_travel=self.always_travel, epic_hunt=self.epic_hunt, epic_hunt_ebs=self.epic_hunt_ebs,
                    rw_def_side=self.rw_def_side, interactive=self.interactive,
                    continuous_fighting=self.continuous_fighting, auto_buy_raw=self.auto_buy_raw,
                    force_wam=self.force_wam, sort_battles_time=self.sort_battles_time, force_travel=self.force_travel,
                    telegram=self.telegram, telegram_chat_id=self.telegram_chat_id, telegram_token=self.telegram_token)


class Energy:
    limit = 500  # energyToRecover
    interval = 10  # energyPerInterval
    recoverable = 0  # energyFromFoodRemaining
    recovered = 0  # energy
    _recovery_time = None

    def __init__(self):
        self._recovery_time = utils.now()

    def __repr__(self):
        return "{:4}/{:4} + {:4}, {:3}hp/6min".format(self.recovered, self.limit, self.recoverable, self.interval)

    def set_reference_time(self, recovery_time: datetime.datetime):
        self._recovery_time = recovery_time.replace(microsecond=0)

    @property
    def food_fights(self):
        return self.available // 10

    @property
    def reference_time(self):
        if self.is_recovered_full or self._recovery_time < utils.now():
            ret = utils.now()
        else:
            ret = self._recovery_time
        return ret

    @property
    def is_recoverable_full(self):
        return self.recoverable >= self.limit - 5 * self.interval

    @property
    def is_recovered_full(self):
        return self.recovered >= self.limit - self.interval

    @property
    def is_energy_full(self):
        return self.is_recoverable_full and self.is_recovered_full

    @property
    def available(self):
        return self.recovered + self.recoverable


class Details:
    xp = 0
    cc = 0
    pp = 0
    pin = None
    gold = 0
    next_pp: List[int] = None
    citizen_id = 0
    citizenship: Country
    current_region = 0
    current_country: Country
    residence_region = 0
    residence_country: Country
    daily_task_done = False
    daily_task_reward = False
    mayhem_skills = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 0, }

    def __init__(self):
        self.next_pp = []

    @property
    def xp_till_level_up(self):
        if self.xp >= 10000:
            next_level_up = (1 + (self.xp // 5000)) * 5000
        elif self.xp >= 7000:
            next_level_up = 10000
        elif self.xp >= 3000:
            next_level_up = (1 + ((self.xp - 1000) // 2000)) * 2000 + 1000
        elif self.xp >= 2000:
            next_level_up = 3000
        elif self.xp >= 450:
            next_level_up = (1 + (self.xp // 500)) * 500
        elif self.xp >= 370:
            next_level_up = (1 + ((self.xp - 10) // 40)) * 40 + 10
        elif self.xp >= 300:
            next_level_up = (1 + ((self.xp - 20) // 35)) * 35 + 20
        elif self.xp >= 150:
            next_level_up = (1 + (self.xp // 30)) * 30
        elif self.xp >= 50:
            next_level_up = (1 + ((self.xp - 10) // 20)) * 20 + 10
        elif self.xp >= 20:
            next_level_up = (1 + ((self.xp - 5) // 15)) * 15 + 5
        else:
            next_level_up = (1 + (self.xp // 10)) * 10
        return next_level_up - self.xp


class Politics:
    is_party_member: bool = False

    party_id: int = 0
    party_slug: str = ""
    is_party_president: bool = False
    is_congressman: bool = False
    is_country_president: bool = False


class House:
    quality = None
    unactivated_count = 0
    active_until = utils.good_timedelta(utils.now(), -datetime.timedelta(days=1))

    def __init__(self, quality: int):
        if 0 < quality < 6:
            self.quality = quality

    @property
    def next_ot_point(self) -> datetime.datetime:
        return self.active_until


class Reporter:
    __to_update: List[Dict[Any, Any]] = None
    key: str = ""
    allowed: bool = False

    @property
    def name(self) -> str:
        return self.citizen.name

    @property
    def email(self) -> str:
        return self.citizen.config.email

    @property
    def citizen_id(self) -> int:
        return self.citizen.details.citizen_id

    @property
    def __dict__(self):
        return dict(name=self.name, email=self.email, citizen_id=self.citizen_id, key=self.key, allowed=self.allowed,
                    queue=self.__to_update)

    def __init__(self, citizen):
        self.citizen = citizen
        self._req = Session()
        self.url = "https://api.erep.lv"
        self._req.headers.update({"user-agent": "Bot reporter v2"})
        self.__to_update = []
        self.__registered: bool = False

    def do_init(self):
        self.key: str = ""
        self.__update_key()
        self.register_account()
        self.allowed = True

    def __update_key(self):
        self.key = hashlib.md5(bytes(f"{self.name}:{self.email}", encoding="UTF-8")).hexdigest()

    def __bot_update(self, data: dict) -> Response:
        if self.__to_update:
            for unreported_data in self.__to_update:
                unreported_data.update(player_id=self.citizen_id, key=self.key)
                self._req.post("{}/bot/update".format(self.url), json=unreported_data)
            self.__to_update.clear()
        r = self._req.post("{}/bot/update".format(self.url), json=data)
        return r

    def register_account(self):
        if not self.__registered:
            try:
                r = self.__bot_update(dict(key=self.key, check=True, player_id=self.citizen_id))
                if not r.json().get("status"):
                    self._req.post("{}/bot/register".format(self.url), json=dict(name=self.name, email=self.email,
                                                                                 player_id=self.citizen_id))
            finally:
                self.__registered = True
                self.allowed = True
                self.report_action("STARTED", value=utils.now().strftime("%F %T"))

    def send_state_update(self, xp: int, cc: float, gold: float, inv_total: int, inv: int,
                          hp_limit: int, hp_interval: int, hp_available: int, food: int, pp: int):

        data = dict(key=self.key, player_id=self.citizen_id, state=dict(
            xp=xp, cc=cc, gold=gold, inv_total=inv_total, inv_free=inv_total - inv, inv=inv, food=food,
            pp=pp, hp_limit=hp_limit, hp_interval=hp_interval, hp_available=hp_available,
        ))

        if self.allowed:
            self.__bot_update(data)

    def report_action(self, action: str, json_val: Dict[Any, Any] = None, value: str = None):
        json_data = dict(
            player_id=getattr(self, 'citizen_id', None), log={'action': action}, key=getattr(self, 'key', None)
        )
        if json_val:
            json_data['log'].update(dict(json=json_val))
        if value:
            json_data['log'].update(dict(value=value))
        if not any([self.key, self.email, self.name, self.citizen_id]):
            return
        if self.allowed:
            self.__bot_update(json_data)
        else:
            self.__to_update.append(json_data)

    def report_promo(self, kind: str, time_until: datetime.datetime):
        self._req.post(f"{self.url}/promos/add/", data=dict(kind=kind, time_untill=time_until))

    def fetch_battle_priorities(self, country: Country) -> List["Battle"]:
        try:
            battle_response = self._req.get(f'{self.url}/api/v1/battles/{country.id}')
            return [self.citizen.all_battles[bid] for bid in battle_response.json().get('battle_ids', []) if
                    bid in self.citizen.all_battles]
        except:  # noqa
            return []

    def fetch_tasks(self) -> Optional[Tuple[str, Tuple[Any]]]:
        try:
            task_response = self._req.get(f'{self.url}/api/v1/command',
                                          params=dict(citizen=self.citizen_id, key=self.key))
            return task_response.json().get('task_collection')
        except:  # noqa
            return


class MyJSONEncoder(json.JSONEncoder):
    def default(self, o):
        from erepublik.citizen import Citizen
        if isinstance(o, Decimal):
            return float(f"{o:.02f}")
        elif isinstance(o, datetime.datetime):
            return dict(__type__='datetime', date=o.strftime("%Y-%m-%d"), time=o.strftime("%H:%M:%S"),
                        tzinfo=str(o.tzinfo) if o.tzinfo else None)
        elif isinstance(o, datetime.date):
            return dict(__type__='date', date=o.strftime("%Y-%m-%d"))
        elif isinstance(o, datetime.timedelta):
            return dict(__type__='timedelta', days=o.days, seconds=o.seconds,
                        microseconds=o.microseconds, total_seconds=o.total_seconds())
        elif isinstance(o, Response):
            return dict(headers=o.headers.__dict__, url=o.url, text=o.text)
        elif hasattr(o, '__dict__'):
            return o.__dict__
        elif isinstance(o, set):
            return list(o)
        elif isinstance(o, Citizen):
            return o.to_json()
        try:
            return super().default(o)
        except Exception as e:  # noqa
            return 'Object is not JSON serializable'


class BattleSide:
    points: int
    deployed: List[Country]
    allies: List[Country]
    battle: "Battle"
    country: Country
    defender: bool

    def __init__(self, battle: "Battle", country: Country, points: int, allies: List[Country], deployed: List[Country],
                 defender: bool):
        self.battle = battle
        self.country = country
        self.points = points
        self.allies = allies
        self.deployed = deployed
        self.defender = defender

    @property
    def id(self) -> int:
        return self.country.id

    def __repr__(self):
        side_text = "Defender" if self.defender else "Invader "
        return f"<BattleSide: {side_text} {self.country.name}|{self.points:02d}p>"

    def __str__(self):
        side_text = "Defender" if self.defender else "Invader "
        return f"{side_text} {self.country.name} - {self.points:02d} points"

    def __format__(self, format_spec):
        return self.country.iso

    @property
    def __dict__(self):
        return dict(points=self.points, country=self.country, defender=self.defender, allies=self.allies,
                    deployed=self.deployed, battle=repr(self.battle))


class BattleDivision:
    id: int
    end: datetime.datetime
    epic: bool
    dom_pts: Dict[str, int]
    wall: Dict[str, Union[int, float]]
    def_medal: Dict[str, int]
    inv_medal: Dict[str, int]
    terrain: int
    div: int
    battle: "Battle"

    @property
    def __dict__(self):
        return dict(id=self.id, division=self.div, terrain=(self.terrain, self.terrain_display), wall=self.wall,
                    dom_pts=self.dom_pts, epic=self.epic, battle=str(self.battle), end=self.div_end)

    @property
    def is_air(self):
        return self.div == 11

    @property
    def div_end(self) -> bool:
        return utils.now() >= self.end

    def __init__(self, battle: "Battle", div_id: int, end: datetime.datetime, epic: bool, div: int, wall_for: int,
                 wall_dom: float, terrain_id: int = 0):
        """Battle division helper class

        :type div_id: int
        :type end: datetime.datetime
        :type epic: bool
        :type div: int
        :type terrain_id: int
        :type wall_for: int
        :type wall_dom: float
        """
        self.battle = battle
        self.id = div_id
        self.end = end
        self.epic = epic
        self.wall = dict({"for": wall_for, "dom": wall_dom})
        self.terrain = terrain_id
        self.div = div

    @property
    def terrain_display(self):
        return _TERRAINS[self.terrain]

    def __str__(self):
        base_name = f"Div #{self.id} d{self.div}"
        if self.terrain:
            base_name += f" ({self.terrain_display})"
        if self.div_end:
            base_name += " Ended"
        return base_name

    def __repr__(self):
        return f"<BattleDivision #{self.id} (battle #{self.battle.id})>"


class Battle:
    id: int
    war_id: int
    zone_id: int
    is_rw: bool
    is_dict_lib: bool
    start: datetime.datetime
    invader: BattleSide
    defender: BattleSide
    div: Dict[int, BattleDivision]
    region_id: int
    region_name: str

    @property
    def __dict__(self):
        return dict(id=self.id, war_id=self.war_id, divisions=self.div, zone=self.zone_id, rw=self.is_rw,
                    dict_lib=self.is_dict_lib, start=self.start, sides={'inv': self.invader, 'def': self.defender},
                    region=[self.region_id, self.region_name])

    @property
    def has_air(self) -> bool:
        for div in self.div.values():
            if div.div == 11:
                return True
        return not bool(self.zone_id % 4)

    @property
    def has_ground(self) -> bool:
        for div in self.div.values():
            if div.div != 11:
                return True
        return bool(self.zone_id % 4)

    @property
    def link(self):
        return f"https://www.erepublik.com/en/military/battlefield/{self.id}"

    def __init__(self, battle: Dict[str, Any]):
        """Object representing eRepublik battle.

        :param battle: Dict object for single battle from '/military/campaignsJson/list' response's 'battles' object
        """

        self.id = int(battle.get('id'))
        self.war_id = int(battle.get('war_id'))
        self.zone_id = int(battle.get('zone_id'))
        self.is_rw = bool(battle.get('is_rw'))
        self.is_as = bool(battle.get('is_as'))
        self.region_id = battle.get('region', {}).get('id')
        self.region_name = battle.get('region', {}).get('name')
        self.start = datetime.datetime.fromtimestamp(int(battle.get('start', 0)), tz=utils.erep_tz)

        self.invader = BattleSide(
            self, COUNTRIES[battle.get('inv', {}).get('id')], battle.get('inv', {}).get('points'),
            [COUNTRIES[row.get('id')] for row in battle.get('inv', {}).get('ally_list')],
            [COUNTRIES[row.get('id')] for row in battle.get('inv', {}).get('ally_list') if row['deployed']], False
        )

        self.defender = BattleSide(
            self, COUNTRIES[battle.get('def', {}).get('id')], battle.get('def', {}).get('points'),
            [COUNTRIES[row.get('id')] for row in battle.get('def', {}).get('ally_list')],
            [COUNTRIES[row.get('id')] for row in battle.get('def', {}).get('ally_list') if row['deployed']], True
        )

        self.div = {}
        for div, data in battle.get('div', {}).items():
            div = int(div)
            if data.get('end'):
                end = datetime.datetime.fromtimestamp(data.get('end'), tz=utils.erep_tz)
            else:
                end = utils.localize_dt(datetime.datetime.max - datetime.timedelta(days=1))

            battle_div = BattleDivision(self, div_id=data.get('id'), div=data.get('div'), end=end,
                                        epic=data.get('epic_type') in [1, 5],
                                        wall_for=data.get('wall').get("for"),
                                        wall_dom=data.get('wall').get("dom"),
                                        terrain_id=data.get('terrain', 0))

            self.div.update({div: battle_div})

    def __str__(self):
        now = utils.now()
        is_started = self.start < utils.now()
        if is_started:
            time_part = " {}".format(now - self.start)
        else:
            time_part = "-{}".format(self.start - now)

        return f"Battle {self.id} for {self.region_name[:16]} | {self.invader} : {self.defender} | Round time {time_part}"

    def __repr__(self):
        return f"<Battle #{self.id} {self.invader}:{self.defender} R{self.zone_id}>"


class EnergyToFight:
    energy: int = 0

    def __init__(self, energy: int = 0):
        self.energy = energy

    def __int__(self):
        return self.energy

    def __str__(self):
        return str(self.energy)

    def __repr__(self):
        return str(self.energy)

    @property
    def i(self):
        return self.__int__()

    @property
    def s(self):
        return self.__str__()

    def check(self, new_energy: int):
        if not isinstance(new_energy, (tuple, int)):
            return self.energy
        if 0 < new_energy < self.energy:
            self.energy = new_energy
        return self.energy


class TelegramBot:
    __initialized: bool = False
    __queue: List[str]
    chat_id: int = 0
    api_url: str = ""
    player_name: str = ""
    __thread_stopper: threading.Event
    _last_time: datetime.datetime
    _last_full_energy_report: datetime.datetime
    _next_time: datetime.datetime
    _threads: List[threading.Thread]

    def __init__(self, stop_event: threading.Event = None):
        self._threads = []
        self.__queue = []
        self.__thread_stopper = threading.Event() if stop_event is None else stop_event
        self._last_full_energy_report = self._last_time = utils.good_timedelta(utils.now(), datetime.timedelta(hours=1))
        self._next_time = utils.now()

    @property
    def __dict__(self):
        return {'chat_id': self.chat_id, 'api_url': self.api_url, 'player': self.player_name,
                'last_time': self._last_time, 'next_time': self._next_time, 'queue': self.__queue,
                'initialized': self.__initialized, 'has_threads': bool(len(self._threads))}

    def do_init(self, chat_id: int, token: str, player_name: str = ""):
        self.chat_id = chat_id
        self.api_url = "https://api.telegram.org/bot{}/sendMessage".format(token)
        self.player_name = player_name
        self.__initialized = True
        self._last_time = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=-5))
        self._last_full_energy_report = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=-30))
        if self.__queue:
            self.send_message("\n\n––––––––––––––––––––––\n\n".join(self.__queue))

    def send_message(self, message: str) -> bool:
        self.__queue.append(message)
        if not self.__initialized:
            if self._last_time < utils.now():
                self.__queue.clear()
            return True
        self._threads = [t for t in self._threads if t.is_alive()]
        self._next_time = utils.good_timedelta(utils.now(), datetime.timedelta(seconds=20))
        if not self._threads:
            name = "telegram_{}send".format(f"{self.player_name}_" if self.player_name else "")
            send_thread = threading.Thread(target=self.__send_messages, name=name)
            send_thread.start()
            self._threads.append(send_thread)

        return True

    def report_full_energy(self, available: int, limit: int, interval: int):
        if (utils.now() - self._last_full_energy_report).total_seconds() >= 30 * 60:
            self._last_full_energy_report = utils.now()
            message = f"Full energy ({available}hp/{limit}hp +{interval}hp/6min)"
            self.send_message(message)

    def report_medal(self, msg, multiple: bool = True):
        new_line = '\n' if multiple else ''
        self.send_message(f"New award: {new_line}*{msg}*")

    def __send_messages(self):
        while self._next_time > utils.now():
            if self.__thread_stopper.is_set():
                break
            self.__thread_stopper.wait(utils.get_sleep_seconds(self._next_time))

        message = "\n\n––––––––––––––––––––––\n\n".join(self.__queue)
        if self.player_name:
            message = f"Player *{self.player_name}*\n" + message
        response = post(self.api_url, json=dict(chat_id=self.chat_id, text=message, parse_mode="Markdown"))
        self._last_time = utils.now()
        if response.json().get('ok'):
            self.__queue.clear()
            return True
        return False


class OfferItem(NamedTuple):
    price: float = 99_999.
    country: Country = Country(0, "", "", "")
    amount: int = 0
    offer_id: int = 0
    citizen_id: int = 0


COUNTRIES: Dict[int, Country] = {
    1: Country(1, 'Romania', 'Romania', 'ROU'), 9: Country(9, 'Brazil', 'Brazil', 'BRA'),
    10: Country(10, 'Italy', 'Italy', 'ITA'), 11: Country(11, 'France', 'France', 'FRA'),
    12: Country(12, 'Germany', 'Germany', 'DEU'), 13: Country(13, 'Hungary', 'Hungary', 'HUN'),
    14: Country(14, 'China', 'China', 'CHN'), 15: Country(15, 'Spain', 'Spain', 'ESP'),
    23: Country(23, 'Canada', 'Canada', 'CAN'), 24: Country(24, 'USA', 'USA', 'USA'),
    26: Country(26, 'Mexico', 'Mexico', 'MEX'), 27: Country(27, 'Argentina', 'Argentina', 'ARG'),
    28: Country(28, 'Venezuela', 'Venezuela', 'VEN'), 29: Country(29, 'United Kingdom', 'United-Kingdom', 'GBR'),
    30: Country(30, 'Switzerland', 'Switzerland', 'CHE'), 31: Country(31, 'Netherlands', 'Netherlands', 'NLD'),
    32: Country(32, 'Belgium', 'Belgium', 'BEL'), 33: Country(33, 'Austria', 'Austria', 'AUT'),
    34: Country(34, 'Czech Republic', 'Czech-Republic', 'CZE'), 35: Country(35, 'Poland', 'Poland', 'POL'),
    36: Country(36, 'Slovakia', 'Slovakia', 'SVK'), 37: Country(37, 'Norway', 'Norway', 'NOR'),
    38: Country(38, 'Sweden', 'Sweden', 'SWE'), 39: Country(39, 'Finland', 'Finland', 'FIN'),
    40: Country(40, 'Ukraine', 'Ukraine', 'UKR'), 41: Country(41, 'Russia', 'Russia', 'RUS'),
    42: Country(42, 'Bulgaria', 'Bulgaria', 'BGR'), 43: Country(43, 'Turkey', 'Turkey', 'TUR'),
    44: Country(44, 'Greece', 'Greece', 'GRC'), 45: Country(45, 'Japan', 'Japan', 'JPN'),
    47: Country(47, 'South Korea', 'South-Korea', 'KOR'), 48: Country(48, 'India', 'India', 'IND'),
    49: Country(49, 'Indonesia', 'Indonesia', 'IDN'), 50: Country(50, 'Australia', 'Australia', 'AUS'),
    51: Country(51, 'South Africa', 'South Africa', 'ZAF'),
    52: Country(52, 'Republic of Moldova', 'Republic-of-Moldova', 'MDA'),
    53: Country(53, 'Portugal', 'Portugal', 'PRT'), 54: Country(54, 'Ireland', 'Ireland', 'IRL'),
    55: Country(55, 'Denmark', 'Denmark', 'DNK'), 56: Country(56, 'Iran', 'Iran', 'IRN'),
    57: Country(57, 'Pakistan', 'Pakistan', 'PAK'), 58: Country(58, 'Israel', 'Israel', 'ISR'),
    59: Country(59, 'Thailand', 'Thailand', 'THA'), 61: Country(61, 'Slovenia', 'Slovenia', 'SVN'),
    63: Country(63, 'Croatia', 'Croatia', 'HRV'), 64: Country(64, 'Chile', 'Chile', 'CHL'),
    65: Country(65, 'Serbia', 'Serbia', 'SRB'), 66: Country(66, 'Malaysia', 'Malaysia', 'MYS'),
    67: Country(67, 'Philippines', 'Philippines', 'PHL'), 68: Country(68, 'Singapore', 'Singapore', 'SGP'),
    69: Country(69, 'Bosnia and Herzegovina', 'Bosnia-Herzegovina', 'BiH'),
    70: Country(70, 'Estonia', 'Estonia', 'EST'), 80: Country(80, 'Montenegro', 'Montenegro', 'MNE'),
    71: Country(71, 'Latvia', 'Latvia', 'LVA'), 72: Country(72, 'Lithuania', 'Lithuania', 'LTU'),
    73: Country(73, 'North Korea', 'North-Korea', 'PRK'), 74: Country(74, 'Uruguay', 'Uruguay', 'URY'),
    75: Country(75, 'Paraguay', 'Paraguay', 'PRY'), 76: Country(76, 'Bolivia', 'Bolivia', 'BOL'),
    77: Country(77, 'Peru', 'Peru', 'PER'), 78: Country(78, 'Colombia', 'Colombia', 'COL'),
    79: Country(79, 'Republic of Macedonia (FYROM)', 'Republic-of-Macedonia-FYROM', 'MKD'),
    81: Country(81, 'Republic of China (Taiwan)', 'Republic-of-China-Taiwan', 'TWN'),
    82: Country(82, 'Cyprus', 'Cyprus', 'CYP'), 167: Country(167, 'Albania', 'Albania', 'ALB'),
    83: Country(83, 'Belarus', 'Belarus', 'BLR'), 84: Country(84, 'New Zealand', 'New-Zealand', 'NZL'),
    164: Country(164, 'Saudi Arabia', 'Saudi-Arabia', 'SAU'), 165: Country(165, 'Egypt', 'Egypt', 'EGY'),
    166: Country(166, 'United Arab Emirates', 'United-Arab-Emirates', 'UAE'),
    168: Country(168, 'Georgia', 'Georgia', 'GEO'), 169: Country(169, 'Armenia', 'Armenia', 'ARM'),
    170: Country(170, 'Nigeria', 'Nigeria', 'NGA'), 171: Country(171, 'Cuba', 'Cuba', 'CUB')
}

_TERRAINS = {0: "Standard", 1: 'Industrial', 2: 'Urban', 3: 'Suburbs', 4: 'Airport', 5: 'Plains', 6: 'Wasteland',
             7: 'Mountains', 8: 'Beach', 9: 'Swamp', 10: 'Mud', 11: 'Hills', 12: 'Jungle', 13: 'Forest', 14: 'Desert'}
