"""Helper functions for OdooRPC operations.

This module extends the base OdooConnection class with helper functions for
common Odoo operations like partner management, state/country lookups, and
file operations.

Typical usage example:
    connection = EqOdooConnection('config.yaml')
    state_id = connection.get_state_id(country_id, state_name)
"""

import base64
import os
import pathlib
from typing import Any

from . import odoo_connection
from .cache import TTLCache, cached_lookup


class EqOdooConnection(odoo_connection.OdooConnection):
    """Extended Odoo connection class with helper functions.

    This class inherits from OdooConnection and adds various helper methods
    for common Odoo operations. Static lookups (countries, states, UoMs, titles,
    categories) are cached via a TTL cache to reduce RPC calls.
    """

    def __init__(self, eq_yaml_path: str, cache_maxsize: int = 256, cache_ttl: float = 3600) -> None:
        """Initializes the connection and the lookup cache.

        Args:
            eq_yaml_path: Path to the YAML configuration file.
            cache_maxsize: Maximum number of cached lookup entries (default: 256).
            cache_ttl: Time-to-live for cache entries in seconds (default: 3600).
        """
        self._lookup_cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl)
        super().__init__(eq_yaml_path)

    def clear_cache(self) -> None:
        """Clear the lookup cache, forcing fresh RPC calls on next access."""
        self._lookup_cache.clear()

    @cached_lookup()
    def get_state_id(self, country_id: int, state_name: str) -> int | None:
        """Returns the state ID (Bundesland) for a given country and state name.

        Args:
            country_id: The ID of the country in Odoo.
            state_name: The name of the state/province/bundesland.

        Returns:
            The ID of the state if found, None otherwise.
        """
        RES_COUNTRY_STATE = self.odoo.env["res.country.state"]
        state_id = RES_COUNTRY_STATE.search([("name", "=", state_name), ("country_id", "=", country_id)])
        return state_id[0] if state_id else None

    def get_res_partner_id(self, supplierno: str | None = None, customerno: str | None = None) -> list[int]:
        """Retrieves partner IDs based on supplier or customer numbers.

        Args:
            supplierno: Optional supplier number to search for.
            customerno: Optional customer number to search for.

        Returns:
            List of matching partner IDs.
        """
        RES_PARTNER = self.odoo.env["res.partner"]
        domain = []

        if supplierno:
            domain.append(("supplier_number", "=", supplierno))
        if customerno:
            domain.append(("customer_number", "=", customerno))

        return RES_PARTNER.search(domain)

    @cached_lookup()
    def get_res_partner_category_id(self, category_name: str) -> int:
        """Gets or creates a partner category (tag).

        If the category doesn't exist, it will be created.

        Args:
            category_name: Name of the category/tag.

        Returns:
            ID of the existing or newly created category.
        """
        RES_PARTNER_CATEGORY = self.odoo.env["res.partner.category"]
        category_ids = RES_PARTNER_CATEGORY.search([("name", "=", category_name)])
        if category_ids:
            return category_ids[0]
        category_data = {"name": category_name}
        return RES_PARTNER_CATEGORY.create(category_data)

    def get_ir_sequence_number_next_actual(self, code: str) -> int | None:
        """Returns the next actual number in the sequence.

        Uses a single search_read RPC call instead of separate search + browse.

        Args:
            code: The code of the sequence.

        Returns:
            The next actual number in the sequence if found, None otherwise.
        """
        result = self.odoo.execute_kw(
            "ir.sequence",
            "search_read",
            [[("code", "=", code)]],
            {"fields": ["number_next_actual"], "limit": 1},
        )
        if result:
            return result[0]["number_next_actual"]
        return None

    @cached_lookup()
    def get_res_partner_title_id(self, title: str) -> int | None:
        """Returns the ID of the partner title.

        Args:
            title: The title to search for.

        Returns:
            The ID of the title if found, None otherwise.
        """
        RES_PARTNER_TITLE = self.odoo.env["res.partner.title"]
        title_id = RES_PARTNER_TITLE.search([("name", "=", title)])
        return title_id[0] if title_id else None

    def set_ir_sequence_number_next_actual(self, code: str, set_value: int) -> bool:
        """Sets the next actual number in the sequence.

        Uses search + write (2 RPC calls) instead of search + browse + write (3-4 calls).

        Args:
            code: The code of the sequence.
            set_value: The new value for the next actual number.

        Returns:
            True if the operation was successful, False otherwise.
        """
        sequence_ids = self.odoo.execute_kw(
            "ir.sequence",
            "search",
            [[("code", "=", code)]],
        )
        if sequence_ids:
            self.odoo.execute_kw(
                "ir.sequence",
                "write",
                [sequence_ids, {"number_next_actual": set_value}],
            )
            return True
        return False

    def set_stock_warehouse_orderpoint(self, product_id: int) -> bool:
        """Sets the reorder point for a product.

        Args:
            product_id: The ID of the product.

        Returns:
            True if the operation was successful, False otherwise.
        """
        STOCK_WAREHOUSE_ORDERPOINT = self.odoo.env["stock.warehouse.orderpoint"]
        orderpoint_id = STOCK_WAREHOUSE_ORDERPOINT.search([("product_id", "=", product_id)])
        if not orderpoint_id:
            orderpoint_data = {
                "product_id": product_id,
                "product_min_qty": 0,
                "product_max_qty": 0,
                "qty_multiple": 1,
            }
            STOCK_WAREHOUSE_ORDERPOINT.create(orderpoint_data)
            return True
        return False

    def get_picture(
        self,
        picture_path: str,
        max_size_mb: int = 50,
        allowed_directory: str | None = None,
    ) -> str | None:
        """Loads a picture from a file path and encodes it in BASE64.

        Args:
            picture_path: The path to the picture file.
            max_size_mb: Maximum allowed file size in megabytes (default: 50).
            allowed_directory: If set, only files within this directory are allowed.

        Returns:
            The BASE64 encoded picture if the file exists, None otherwise.

        Raises:
            ValueError: If the file exceeds the size limit or is outside the allowed directory.
        """
        path = pathlib.Path(picture_path).resolve()

        if not path.is_file():
            return None

        if allowed_directory is not None:
            allowed = pathlib.Path(allowed_directory).resolve()
            if not str(path).startswith(str(allowed) + os.sep) and path != allowed:
                raise ValueError(f"Access denied: '{path}' is outside allowed directory '{allowed}'")

        file_size = path.stat().st_size
        if file_size > max_size_mb * 1024 * 1024:
            raise ValueError(f"File size ({file_size / (1024 * 1024):.1f}MB) exceeds {max_size_mb}MB limit")

        with open(path, "rb") as f:
            return str(base64.b64encode(f.read()).decode("utf-8"))

    @cached_lookup()
    def get_product_uom_id(self, uom: str) -> int:
        """Returns the ID of the product unit of measure.

        Args:
            uom: The unit of measure to search for.

        Returns:
            The ID of the unit of measure if found, 1 (default) otherwise.
        """
        if self.odoo_version in [10, 11, 12]:
            PRODUCT_UOM = self.odoo.env["product.uom"]
        else:
            PRODUCT_UOM = self.odoo.env["uom.uom"]
        uom_id = PRODUCT_UOM.search([("name", "=", uom)])
        return uom_id[0] if uom_id else 1

    def string_contains_numbers(self, source: str) -> bool:
        """Checks if a string contains numbers.

        Args:
            source: The string to check.

        Returns:
            True if the string contains numbers, False otherwise.
        """
        return any(i.isdigit() for i in source)

    def extract_street_address_part(self, street_infos: str) -> tuple[str, str]:
        """Extracts street and house number from a string.

        Args:
            street_infos: The string containing street and house number.

        Returns:
            A tuple containing (street, house_number).
        """
        street = street_infos
        house_no = ""

        if len(street_infos) > 0:
            street_parts = street_infos.split(" ")
            if len(street_parts) == 2:
                street = street_parts[0]
                house_no = street_parts[1]
            elif len(street_parts) > 2:
                street = ""
                part_position = 1
                for part in street_parts:
                    if part_position == len(street_parts):
                        if self.string_contains_numbers(part):  # beinhaltet die letzte Position wirklich eine Zahl
                            house_no = part  # ja, es ist typische Adresse -> Weiherstrasse 12
                        else:
                            street += part  # nein, es ist z.B. eine GB Adresse -> Flat 42A Ashburnham Mansions
                    else:
                        street += part + " "

                    part_position += 1

        street = street.strip()
        house_no = house_no.strip()
        return street, house_no

    def check_if_company_exists(self, company_name: str, zip_code: str, city: str) -> int | None:
        """Checks if a company exists in the res_partner table.

        Args:
            company_name: The name of the company.
            zip_code: The zip code of the company.
            city: The city of the company.

        Returns:
            The ID of the company if found, None otherwise.
        """
        RES_PARTNER = self.odoo.env["res.partner"]
        record = RES_PARTNER.search(
            [("name", "like", company_name), ("zip", "=", zip_code), ("city", "=", city), ("is_company", "=", True)]
        )
        return record[0] if record else None

    # ==================== NEW METHODS ====================

    def get_country_id(self, country_name: str) -> int | None:
        """Returns the country ID for a given country name.

        Args:
            country_name: The name of the country (e.g., 'Germany', 'Deutschland').

        Returns:
            The ID of the country if found, None otherwise.
        """
        RES_COUNTRY = self.odoo.env["res.country"]
        country_ids = RES_COUNTRY.search([("name", "=", country_name)])
        if not country_ids:
            # Try case-insensitive search
            country_ids = RES_COUNTRY.search([("name", "ilike", country_name)])
        return country_ids[0] if country_ids else None

    @cached_lookup()
    def get_country_id_by_code(self, country_code: str) -> int | None:
        """Returns the country ID for a given ISO country code.

        Args:
            country_code: The ISO 3166-1 alpha-2 country code (e.g., 'DE', 'US').

        Returns:
            The ID of the country if found, None otherwise.
        """
        RES_COUNTRY = self.odoo.env["res.country"]
        country_ids = RES_COUNTRY.search([("code", "=", country_code.upper())])
        return country_ids[0] if country_ids else None

    def create_partner(
        self,
        name: str,
        is_company: bool = False,
        email: str | None = None,
        phone: str | None = None,
        street: str | None = None,
        city: str | None = None,
        zip_code: str | None = None,
        country_id: int | None = None,
        **kwargs: Any,
    ) -> int:
        """Creates a new partner (contact or company) in Odoo.

        Args:
            name: The name of the partner.
            is_company: Whether this is a company (True) or individual (False).
            email: Email address.
            phone: Phone number.
            street: Street address.
            city: City name.
            zip_code: ZIP/postal code.
            country_id: ID of the country.
            **kwargs: Additional fields to set on the partner.

        Returns:
            The ID of the newly created partner.
        """
        RES_PARTNER = self.odoo.env["res.partner"]

        partner_data = {
            "name": name,
            "is_company": is_company,
        }

        if email:
            partner_data["email"] = email
        if phone:
            partner_data["phone"] = phone
        if street:
            partner_data["street"] = street
        if city:
            partner_data["city"] = city
        if zip_code:
            partner_data["zip"] = zip_code
        if country_id:
            partner_data["country_id"] = country_id

        # Add any additional fields
        partner_data.update(kwargs)

        return RES_PARTNER.create(partner_data)

    def get_product_by_ref(self, default_code: str) -> int | None:
        """Returns the product ID for a given internal reference (default_code).

        Args:
            default_code: The internal reference/SKU of the product.

        Returns:
            The ID of the product if found, None otherwise.
        """
        PRODUCT_PRODUCT = self.odoo.env["product.product"]
        product_ids = PRODUCT_PRODUCT.search([("default_code", "=", default_code)])
        return product_ids[0] if product_ids else None

    def get_product_template_by_ref(self, default_code: str) -> int | None:
        """Returns the product template ID for a given internal reference.

        Args:
            default_code: The internal reference/SKU of the product.

        Returns:
            The ID of the product template if found, None otherwise.
        """
        PRODUCT_TEMPLATE = self.odoo.env["product.template"]
        template_ids = PRODUCT_TEMPLATE.search([("default_code", "=", default_code)])
        return template_ids[0] if template_ids else None

    def execute_method(
        self,
        model: str,
        method: str,
        record_ids: list[int] | None = None,
        args: list[Any] | None = None,
        kwargs: dict[str, Any] | None = None,
    ) -> Any:
        """Executes a method on an Odoo model via RPC.

        This is a generic method to call any Odoo model method.

        Args:
            model: The Odoo model name (e.g., 'res.partner').
            method: The method name to call (e.g., 'name_search').
            record_ids: Optional list of record IDs to call the method on.
            args: Optional positional arguments for the method.
            kwargs: Optional keyword arguments for the method.

        Returns:
            The result of the method call.

        Raises:
            ValueError: If the method name starts with '_' (private/magic methods).
        """
        if method.startswith("_"):
            raise ValueError(f"Private method '{method}' not allowed")

        Model = self.odoo.env[model]

        if record_ids:
            records = Model.browse(record_ids)
            if args and kwargs:
                return getattr(records, method)(*args, **kwargs)
            elif args:
                return getattr(records, method)(*args)
            elif kwargs:
                return getattr(records, method)(**kwargs)
            else:
                return getattr(records, method)()
        else:
            if args and kwargs:
                return getattr(Model, method)(*args, **kwargs)
            elif args:
                return getattr(Model, method)(*args)
            elif kwargs:
                return getattr(Model, method)(**kwargs)
            else:
                return getattr(Model, method)()

    def search_read(
        self,
        model: str,
        domain: list | None = None,
        fields: list[str] | None = None,
        limit: int | None = None,
        offset: int = 0,
        order: str | None = None,
    ) -> list[dict[str, Any]]:
        """Searches for records and returns specified fields in a single RPC call.

        Uses the native Odoo ``search_read`` method via ``execute_kw`` which
        performs the search and read in one server-side operation instead of
        two separate RPC calls.

        Args:
            model: The Odoo model name (e.g., 'res.partner').
            domain: Search domain (e.g., [('is_company', '=', True)]).
            fields: List of fields to return (e.g., ['name', 'email']).
            limit: Maximum number of records to return.
            offset: Number of records to skip.
            order: Sort order (e.g., 'name asc').

        Returns:
            List of dictionaries containing the requested fields.
        """
        search_domain = domain or []
        search_fields = fields or ["id", "name"]

        kwargs: dict[str, Any] = {"fields": search_fields, "offset": offset}
        if limit is not None:
            kwargs["limit"] = limit
        if order is not None:
            kwargs["order"] = order

        return self.odoo.execute_kw(model, "search_read", [search_domain], kwargs) or []
