"""Models related to the houses section in Tibia.com."""
import datetime
import re
from typing import Dict, List, Optional

from tibiapy import abc
from tibiapy.enums import HouseOrder, HouseStatus, HouseType, Sex
from tibiapy.errors import InvalidContent
from tibiapy.utils import get_tibia_url, parse_tibia_datetime, parse_tibia_money, \
    parse_tibiacom_content, parse_tibiacom_tables, try_date, try_datetime, try_enum

__all__ = (
    "HousesSection",
    "House",
    "CharacterHouse",
    "GuildHouse",
    "HouseEntry",
)

id_regex = re.compile(r'house_(\d+)\.')
bed_regex = re.compile(r'This (?P<type>\w+) can have up to (?P<beds>[\d-]+) bed')
info_regex = \
    re.compile(r'The house has a size of (?P<size>\d+) square meter[s]?. '
               r'The monthly rent is (?P<rent>\d+k?) gold and will be debited to the bank account on (?P<world>\w+).')

rented_regex = re.compile(r'The house has been rented by (?P<owner>[^.]+)\.'
                          r' (?P<pronoun>\w+) has paid the rent until (?P<paid_until>[^.]+)\.')
transfer_regex = re.compile(r'\w+ will move out on (?P<transfer_date>[^(]+)\([^)]+\)(?: and (?P<verb>wants to|will)'
                            r' pass the house to (?P<transferee>[\w\s]+) for (?P<transfer_price>\d+) gold coin)?')
moving_regex = re.compile(r'\w+ will move out on (?P<move_date>[^(]+)')
bid_regex = \
    re.compile(r'The highest bid so far is (?P<highest_bid>\d+) gold and has been submitted by (?P<bidder>[^.]+)')
auction_regex = re.compile(r'The auction (?P<auction_state>has ended|will end) at (?P<auction_end>[^.]+).')

list_header_regex = re.compile(r'Available (?P<type>[\w\s]+) in (?P<town>[\w\s\']+) on (?P<world>\w+)')
list_auction_regex = re.compile(r'\((?P<bid>\d+) gold; (?P<time_left>\w)+ (?P<time_unit>day|hour)s? left\)')


class HousesSection(abc.Serializable):
    """Represents the house section.

    .. versionadded:: 5.0.0

    Attributes
    ----------
    world: :class:`str`
        The selected world to show houses for.
    town: :class:`str`
        The town to show houses for.
    status: :class:`HouseStatus`
        The status to show. If :obj:`None`, any status is shown.
    house_type: :class:`HouseType`
        The type of houses to show.
    order: :class:`HouseOrder`
        The ordering to use for the results.
    entries: :class:`list` of :class:`HouseEntry`
        The houses matching the filters.
    available_worlds: :class:`list` of :class:`str`
        The list of available worlds to choose from.
    available_towns: :class:`list` of :class:`str`
        The list of available towns to choose from.
    """

    __slots__ = (
        "world",
        "town",
        "status",
        "house_type",
        "order",
        "entries",
        "available_worlds",
        "available_towns",
    )

    def __init__(self, **kwargs):
        self.world: str = kwargs.get("world")
        self.town: str = kwargs.get("town")
        self.status: Optional[HouseStatus] = kwargs.get("status")
        self.house_type: HouseType = kwargs.get("house_type")
        self.order: HouseOrder = kwargs.get("order", HouseOrder.NAME)
        self.available_worlds: List[str] = kwargs.get("available_worlds", [])
        self.available_towns: List[str] = kwargs.get("available_towns", [])
        self.entries: List[HouseEntry] = kwargs.get("entries", [])

    def __repr__(self):
        return f"<{self.__class__.__name__} world={self.world!r} town={self.town!r} house_type={self.house_type!r}>"

    @property
    def url(self):
        """:class:`str`: Get the URL to the houses section with the current parameters."""
        return self.get_url(self.world, self.town, self.house_type, self.status, self.order)

    @classmethod
    def _get_query_params(cls, world, town, house_type, status=None, order=None) -> Dict[str, str]:
        """Build the query parameters for a house search.

        Parameters
        ----------
        world: :class:`str`
            The world to search for houses in.
        town: :class:`str`
            The town to search houses in.
        house_type: :class:`HouseType`
            The type of houses to filter.
        status: :class:`HouseStatus`, optional
            The status of the house to filter. If undefined, all status will be included.
        order: :class:`HouseOrder`
            The field to order houses by.

        Returns
        -------
        :class:`dict`
            The query parameters representing for a house search.
        """
        params = {
            "world": world,
            "town": town,
            "type": house_type.plural if house_type else None,
            "status": status.value if status else None,
            "order": order.value if order else None,
        }
        return {k: v for k, v in params.items() if v is not None}

    @classmethod
    def get_url(cls, world, town, house_type, status=None, order=None):
        """Get the URL to the house list on Tibia.com with the specified filters.

        Parameters
        ----------
        world: :class:`str`
            The world to search in.
        town: :class:`str`
            The town to show results for.
        house_type: :class:`HouseType`
            The type of houses to show.
        status: :class:`HouseStatus`
            The status of the houses to show.
        order: :class:`HouseOrder`
            The sorting parameter to use for the results.

        Returns
        -------
        :class:`str`
            The URL to the list matching the parameters.
        """
        query = cls._get_query_params(world, town, house_type, status, order)
        return get_tibia_url("community", "houses", **query)

    @classmethod
    def from_content(cls, content):
        """Parse the content of a house list from Tibia.com into a list of houses.

        Parameters
        ----------
        content: :class:`str`
            The raw HTML response from the house list.

        Returns
        -------
        :class:`HouseSection`
            The houses found in the page.

        Raises
        ------
        InvalidContent`
            Content is not the house list from Tibia.com
        """
        try:
            parsed_content = parse_tibiacom_content(content)
            tables = parse_tibiacom_tables(parsed_content)
            house_results = cls()
            house_results._parse_filters(tables["House Search"])
            if len(tables) < 2:
                return house_results
            houses_table = tables[list(tables.keys())[0]]
            _, *rows = houses_table.find_all("tr")
            for row in rows[1:]:
                cols = row.find_all("td")
                if len(cols) != 5:
                    continue
                name = cols[0].text.replace('\u00a0', ' ')
                house = HouseEntry(name, house_results.world, 0, town=house_results.town,
                                   type=house_results.house_type)
                size = cols[1].text.replace('sqm', '')
                house.size = int(size)
                rent = cols[2].text.replace('gold', '')
                house.rent = parse_tibia_money(rent)
                status = cols[3].text.replace('\xa0', ' ')
                house._parse_status(status)
                id_input = cols[4].find("input", {'name': 'houseid'})
                house.id = int(id_input["value"])
                house_results.entries.append(house)
            return house_results
        except (ValueError, AttributeError, KeyError) as e:
            raise InvalidContent("content does not belong to a Tibia.com house list", e)

    def _parse_filters(self, filters_table):
        world_select = filters_table.find("select", {"name": "world"})
        for world_option in world_select.find_all("option"):
            world_name = world_option.text
            if "(" in world_name:
                continue
            self.available_worlds.append(world_name)
            if world_option.attrs.get("selected"):
                self.world = world_name
        for town_option in filters_table.find_all("input", {"name": "town"}):
            town_name = town_option.attrs.get("value")
            self.available_towns.append(town_name)
            if "checked" in town_option.attrs:
                self.town = town_name
        checked_status = filters_table.find("input", {"name": "state", "checked": True})
        if checked_status.attrs.get("value"):
            self.status = try_enum(HouseStatus, checked_status.attrs.get("value"))
        checked_type = filters_table.find("input", {"name": "type", "checked": True})
        if checked_type.attrs.get("value"):
            self.house_type = try_enum(HouseType, checked_type.attrs.get("value")[:-1])
        checked_order = filters_table.find("input", {"name": "order", "checked": True})
        if checked_order and checked_order.attrs.get("value"):
            self.order = try_enum(HouseOrder, checked_order.attrs.get("value"), HouseOrder.NAME)
        return self


class House(abc.BaseHouse, abc.HouseWithId, abc.Serializable):
    """Represents a house in a specific world.

    Attributes
    ----------
    id: :class:`int`
        The internal ID of the house. This is used on the website to identify houses.
    name: :class:`str`
        The name of the house.
    world: :class:`str`
        The name of the world the house belongs to.
    status: :class:`HouseStatus`
        The current status of the house.
    type: :class:`HouseType`
        The type of the house.
    image_url: :class:`str`
        The URL to the house's minimap image.
    beds: :class:`int`
        The number of beds the house has.
    size: :class:`int`
        The number of SQM the house has.
    rent: :class:`int`
        The monthly cost paid for the house, in gold coins.
    owner: :class:`str`
        The current owner of the house, if any.
    owner_sex: :class:`Sex`
        The sex of the owner of the house, if applicable.
    paid_until: :class:`datetime.datetime`, optional
        The date the last paid rent is due.
    transfer_date: :class:`datetime.datetime`, optional
        The date when the owner will move out of the house, if applicable.
    transferee: :class:`str`, optional
        The character who will receive the house when the owner moves, if applicable.
    transfer_price: :class:`int`
        The price that will be paid from the transferee to the owner for the house transfer.
    transfer_accepted: :class:`bool`
        Whether the house transfer has already been accepted or not.
    highest_bid: :class:`int`
        The currently highest bid on the house if it is being auctioned.
    highest_bidder: :class:`str`, optional
        The character that holds the highest bid.
    auction_end: :class:`datetime.datetime`, optional
        The date when the auction will end.
    """

    __slots__ = (
        "id",
        "name",
        "world",
        "status",
        "type",
        "image_url",
        "beds",
        "type",
        "size",
        "rent",
        "owner",
        "owner_sex",
        "paid_until",
        "transfer_date",
        "transferee",
        "transfer_price",
        "transfer_accepted",
        "highest_bid",
        "highest_bidder",
        "auction_end",
    )

    def __init__(self, name, world=None, **kwargs):
        self.id: int = kwargs.get("id", 0)
        self.name: str = name
        self.world: str = world
        self.image_url: str = kwargs.get("image_url")
        self.beds: int = kwargs.get("beds", 0)
        self.type: HouseType = try_enum(HouseType, kwargs.get("type"), HouseType.HOUSE)
        self.size: int = kwargs.get("size", 0)
        self.rent: int = kwargs.get("rent", 0)
        self.status: HouseStatus = try_enum(HouseStatus, kwargs.get("status"), None)
        self.owner: Optional[str] = kwargs.get("owner")
        self.owner_sex: Optional[str] = try_enum(Sex, kwargs.get("owner_sex"))
        self.paid_until: datetime.datetime = try_datetime(kwargs.get("paid_until"))
        self.transfer_date: datetime.datetime = try_datetime(kwargs.get("transfer_date"))
        self.transferee: Optional[str] = kwargs.get("transferee")
        self.transfer_price: int = kwargs.get("transfer_price", 0)
        self.transfer_accepted: bool = kwargs.get("transfer_accepted", False)
        self.highest_bid: int = kwargs.get("highest_bid", 0)
        self.highest_bidder: Optional[str] = kwargs.get("highest_bidder")
        self.auction_end: datetime.datetime = try_datetime(kwargs.get("auction_end"))

    # region Properties
    @property
    def owner_url(self):
        """:class:`str`: The URL to the Tibia.com page of the house's owner, if applicable."""
        return abc.BaseCharacter.get_url(self.owner) if self.owner is not None else None

    @property
    def transferee_url(self):
        """:class:`str`: The URL to the Tibia.com page of the character receiving the house, if applicable."""
        return abc.BaseCharacter.get_url(self.transferee) if self.transferee is not None else None

    @property
    def highest_bidder_url(self):
        """:class:`str`: The URL to the Tibia.com page of the character with the highest bid, if applicable."""
        return abc.BaseCharacter.get_url(self.highest_bidder) if self.highest_bidder is not None else None
    # endregion

    # region Public methods
    @classmethod
    def from_content(cls, content):
        """Parse a Tibia.com response into a House object.

        Parameters
        ----------
        content: :class:`str`
            HTML content of the page.

        Returns
        -------
        :class:`House`
            The house contained in the page, or None if the house doesn't exist.

        Raises
        ------
        InvalidContent
            If the content is not the house section on Tibia.com
        """
        parsed_content = parse_tibiacom_content(content)
        image_column, desc_column, *_ = parsed_content.find_all('td')
        if "Error" in image_column.text:
            return None
        image = image_column.find('img')
        for br in desc_column.find_all("br"):
            br.replace_with("\n")
        description = desc_column.text.replace("\u00a0", " ").replace("\n\n", "\n")
        lines = description.splitlines()
        try:
            name, beds, info, state, *_ = lines
        except ValueError:
            raise InvalidContent("content does is not from the house section of Tibia.com")

        house = cls(name.strip())
        house.image_url = image["src"]
        house.id = int(id_regex.search(house.image_url).group(1))
        m = bed_regex.search(beds)
        if m:
            if m.group("type").lower() in ["guildhall", "clanhall"]:
                house.type = HouseType.GUILDHALL
            else:
                house.type = HouseType.HOUSE
            house.beds = int(m.group("beds"))

        m = info_regex.search(info)
        if m:
            house.world = m.group("world")
            house.rent = parse_tibia_money(m.group("rent"))
            house.size = int(m.group("size"))

        house._parse_status(state)
        return house
    # endregion

    def _parse_status(self, status):
        """Parse the house's state description and applies the corresponding values.

        Parameters
        ----------
        status: :class:`str`
            Plain text string containing the current renting state of the house.
        """
        m = rented_regex.search(status)
        if m:
            self.status = HouseStatus.RENTED
            self.owner = m.group("owner")
            self.owner_sex = Sex.MALE if m.group("pronoun") == "He" else Sex.FEMALE
            self.paid_until = parse_tibia_datetime(m.group("paid_until"))
        else:
            self.status = HouseStatus.AUCTIONED

        m = transfer_regex.search(status)
        if m:
            self.transfer_date = parse_tibia_datetime(m.group("transfer_date"))
            self.transfer_accepted = m.group("verb") == "will"
            self.transferee = m.group("transferee")
            price = m.group("transfer_price")
            self.transfer_price = int(price) if price is not None else 0

        m = auction_regex.search(status)
        if m:
            self.auction_end = parse_tibia_datetime(m.group("auction_end"))

        m = bid_regex.search(status)
        if m:
            self.highest_bid = int(m.group("highest_bid"))
            self.highest_bidder = m.group("bidder")


class CharacterHouse(abc.BaseHouse, abc.HouseWithId, abc.Serializable):
    """A house owned by a character.

    Attributes
    ----------
    id: :class:`int`
        The internal ID of the house. This is used on the website to identify houses.
    name: :class:`str`
        The name of the house.
    world: :class:`str`
        The name of the world the house belongs to.
    status: :class:`HouseStatus`
        The current status of the house.
    type: :class:`HouseType`
        The type of the house.
    town: :class:`str`
        The town where the city is located in.
    owner: :class:`str`
        The owner of the house.
    paid_until_date: :class:`datetime.date`
        The date the last paid rent is due.
    """

    __slots__ = (
        "id",
        "name",
        "world",
        "status",
        "type",
        "town",
        "owner",
        "paid_until_date",
    )

    def __init__(self, _id, name, world=None, town=None, owner=None, paid_until_date=None):
        self.id = int(_id)
        self.name: str = name
        self.town: str = town
        self.world: str = world
        self.owner: str = owner
        self.paid_until_date = try_date(paid_until_date)
        self.status = HouseStatus.RENTED
        self.type = HouseType.HOUSE


class GuildHouse(abc.BaseHouse, abc.Serializable):
    """A guildhall owned by a guild.

    By limitation of Tibia.com, the ID of the guildhall is not available.

    Attributes
    ----------
    name: :class:`str`
        The name of the house.
    world: :class:`str`
        The name of the world the house belongs to.
    status: :class:`HouseStatus`
        The current status of the house.

        This is kept for compatibility with house objects and will always be :obj:`HouseStatus.RENTED`.
    type: :class:`HouseType`
        The type of the house.

        This is kept for compatibility with house objects and will always be :obj:`HouseType.GUILDHALL`.
    owner: :class:`str`
        The owner of the guildhall.
    paid_until_date: :class:`datetime.date`
        The date the last paid rent is due.
    """

    __slots__ = (
        "name",
        "world",
        "status",
        "owner",
        "paid_until_date",
    )

    def __init__(self, name, world=None, owner=None, paid_until_date=None):
        self.name: str = name
        self.world: str = world
        self.owner: str = owner
        self.paid_until_date: datetime.date = try_date(paid_until_date)
        self.status: HouseStatus = HouseStatus.RENTED
        self.type: HouseType = HouseType.GUILDHALL

    def __repr__(self):
        return "<%s name=%r>" % (self.__class__.__name__, self.name)


class HouseEntry(abc.BaseHouse, abc.HouseWithId, abc.Serializable):
    """Represents a house from the house list in Tibia.com.

    Attributes
    ----------
    id: :class:`int`
        The internal ID of the house. This is used on the website to identify houses.
    name: :class:`str`
        The name of the house.
    world: :class:`str`
        The name of the world the house belongs to.
    status: :class:`HouseStatus`
        The current status of the house.
    type: :class:`HouseType`
        The type of house.
    town: :class:`str`
        The town where the house is located.
    size: :class:`int`
        The size of the house in SQM.
    rent: :class:`int`
        The monthly cost of the house, in gold coins.
    time_left: :class:`datetime.timedelta`, optional
        The number of days or hours left until the bid ends, if it has started.
        This is not an exact measure, it is rounded to hours or days.
    highest_bid: :class:`int`, optional.
        The highest bid so far, if the auction has started.
    """

    __slots__ = (
        "id",
        "name",
        "world",
        "status",
        "type",
        "town",
        "size",
        "rent",
        "time_left",
        "highest_bid",
    )

    def __init__(self, name, world, houseid, **kwargs):
        self.name: str = name
        self.id = int(houseid)
        self.world: str = world
        self.status = try_enum(HouseStatus, kwargs.get("status"))
        self.type = try_enum(HouseType, kwargs.get("type"))
        self.town: str = kwargs.get("town")
        self.size: int = kwargs.get("size", 0)
        self.rent: int = kwargs.get("rent", 0)
        self.time_left: Optional[datetime.timedelta] = kwargs.get("time_left")
        self.highest_bid: Optional[int] = kwargs.get("highest_bid", None)

    # region Private methods
    def _parse_status(self, status):
        """
        Parse the status string found in the table and applies the corresponding values.

        Parameters
        ----------
        status: :class:`str`
            The string containing the status.
        """
        if "rented" in status:
            self.status = HouseStatus.RENTED
        else:
            m = list_auction_regex.search(status)
            if m:
                self.highest_bid = int(m.group('bid'))
                if m.group("time_unit") == "day":
                    self.time_left = datetime.timedelta(days=int(m.group("time_left")))
                else:
                    self.time_left = datetime.timedelta(hours=int(m.group("time_left")))
            self.status = HouseStatus.AUCTIONED
    # endregion
