import httpx
import logging
import asyncio
import urllib.parse
import json
import codecs
from typing import Dict, Optional, Any, Literal, ClassVar
from cachetools import TTLCache
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
)
from ratelimit import limits, sleep_and_retry


# Map of content type IDs to their human-readable names
CONTENTTYPE_ID_MAP = {
    "76": "Tourist Attraction",
    "78": "Cultural Facility",
    "85": "Festival Event",
    "75": "Leisure Activity",
    "80": "Accommodation",
    "79": "Shopping",
    "82": "Restaurant",
    "77": "Transportation",
}

# Map of supported languages to their service endpoints
LANGUAGE_SERVICE_MAP = {
    "en": "EngService2",  # English
    "jp": "JpnService2",  # Japanese
    "zh-cn": "ChsService2",  # Simplified Chinese
    "zh-tw": "ChtService2",  # Traditional Chinese
    "de": "GerService2",  # German
    "fr": "FreService2",  # French
    "es": "SpnService2",  # Spanish
    "ru": "RusService2",  # Russian
}


def decode_unicode_escapes(obj: Any) -> Any:
    """
    Recursively decode unicode escape sequences in strings within data structures.
    This helps resolve Korean character encoding issues like \\uc11c\\uc6b8 -> 서울.
    Only processes strings that actually contain unicode escape sequences.
    """
    if isinstance(obj, str):
        # Only attempt decoding if the string contains unicode escape sequences
        if "\\u" in obj:
            try:
                # Try to decode unicode escape sequences (e.g., \\uc11c\\uc6b8 -> 서울)
                return codecs.decode(obj, "unicode_escape")
            except (UnicodeDecodeError, ValueError):
                # If decoding fails, return original string
                return obj
        else:
            # If no escape sequences, return as-is
            return obj
    elif isinstance(obj, dict):
        return {key: decode_unicode_escapes(value) for key, value in obj.items()}
    elif isinstance(obj, list):
        return [decode_unicode_escapes(item) for item in obj]
    else:
        return obj


class TourismApiError(Exception):
    """Base exception for Tourism API errors"""

    def __init__(
        self,
        message: str,
        response: Optional[httpx.Response] = None,
        request: Optional[httpx.Request] = None,
    ):
        super().__init__(message)
        self.message = message
        self.response = response
        self.request = request

    def __str__(self) -> str:
        base_str = super().__str__()
        if self.response:
            base_str += f" (Status Code: {self.response.status_code})"
        if self.request:
            base_str += f" (Request URL: {self.request.url})"
        return base_str


class TourismApiConnectionError(TourismApiError):
    """Connection error with Tourism API"""

    # Usually no response, so focus on request
    def __init__(self, message: str, request: Optional[httpx.Request] = None):
        super().__init__(message, response=None, request=request)


class TourismApiClientError(TourismApiError):
    """Client-side error with Tourism API requests (4xx)"""

    pass


class TourismApiServerError(TourismApiError):
    """Server-side error with Tourism API operations (5xx)"""

    pass


class KoreaTourismApiClient:
    """
    Client for the Korea Tourism Organization API with caching and rate limiting.

    Features:
    - Multi-language support
    - Response caching with TTL
    - Rate limiting to respect API quotas
    - Automatic retries for transient errors
    - Connection pooling
    """

    # Base URL for all services
    BASE_URL = "http://apis.data.go.kr/B551011"

    # --- Constants for API Parameters ---
    DEFAULT_NUM_OF_ROWS = 100
    DEFAULT_PAGE_NO = 1
    MOBILE_OS = "ETC"
    MOBILE_APP = "MobileApp"
    RESPONSE_FORMAT = "json"
    ARRANGE_MODIFIED_WITH_IMAGE = "Q"  # Sort by modified date with image
    # --- End Constants ---

    # Common endpoints (will be prefixed with service name)
    AREA_BASED_LIST_ENDPOINT = "/areaBasedList2"
    LOCATION_BASED_LIST_ENDPOINT = "/locationBasedList2"
    SEARCH_KEYWORD_ENDPOINT = "/searchKeyword2"
    SEARCH_FESTIVAL_ENDPOINT = "/searchFestival2"
    SEARCH_STAY_ENDPOINT = "/searchStay2"
    DETAIL_COMMON_ENDPOINT = "/detailCommon2"
    DETAIL_INTRO_ENDPOINT = "/detailIntro2"
    DETAIL_INFO_ENDPOINT = "/detailInfo2"
    DETAIL_IMAGE_ENDPOINT = "/detailImage2"
    AREA_BASED_SYNC_LIST_ENDPOINT = "/areaBasedSyncList2"
    AREA_CODE_LIST_ENDPOINT = "/areaCode2"
    CATEGORY_CODE_LIST_ENDPOINT = "/categoryCode2"

    # Class-level connection pool and semaphore for concurrency control
    _shared_client: ClassVar[Optional[httpx.AsyncClient]] = None
    _client_lock: ClassVar[asyncio.Lock] = asyncio.Lock()
    # Make concurrency limit configurable
    # _request_semaphore: ClassVar[asyncio.Semaphore] = asyncio.Semaphore(10)

    def __init__(
        self,
        api_key: str,
        language: str = "en",
        cache_ttl: int = 86400,
        rate_limit_calls: int = 5,
        rate_limit_period: int = 1,
        concurrency_limit: int = 10,
    ):
        """
        Initialize with API key and optional configurations.

        Args:
            api_key: The API key for the Korea Tourism Organization.
            language: Default language code for content (en, ko, jp, etc.).
            cache_ttl: Time-to-live for cached responses in seconds.
            rate_limit_calls: Maximum number of API calls allowed.
            rate_limit_period: Time period (in seconds) for the rate limit.
            concurrency_limit: Maximum number of concurrent API requests.
        """
        self.api_key = api_key
        if (
            not self.api_key or self.api_key == "missing_api_key"
        ):  # Check for placeholder too
            # Log warning here instead of raising immediately, allows lazy init check
            logging.warning("Korean Tourism API key is missing or invalid.")
            # We don't raise here because get_api_client in server.py handles this

        self._logger_name = "tourism_api_client"

        # Store configuration parameters
        self.default_language = language.lower()
        self.language: Optional[str] = None  # Initialize language attribute
        self._cache_ttl = cache_ttl
        self._rate_limit_calls = rate_limit_calls
        self._rate_limit_period = rate_limit_period
        self._concurrency_limit = concurrency_limit

        # Lazy initialization flags/placeholders
        self.service_name: Optional[str] = None
        self.full_base_url: Optional[str] = None
        self._is_fully_initialized = False
        self._cache: Optional[TTLCache] = None
        self._request_semaphore: Optional[asyncio.Semaphore] = None
        self.logger: Optional[logging.Logger] = None  # Add logger type hint

    def _ensure_full_initialization(self):
        """Ensure all initialization tasks are completed before first API request"""
        if self._is_fully_initialized:
            return

        # Get logger first
        self.logger = logging.getLogger(self._logger_name)
        if (
            not self.logger.hasHandlers()
        ):  # Basic check to prevent duplicate handlers if called multiple times
            logging.basicConfig(level=logging.INFO)  # Basic config if not already set

        # Validate API key presence here before proceeding
        if not self.api_key or self.api_key == "missing_api_key":
            self.logger.error("Cannot proceed without a valid KOREA_TOURISM_API_KEY.")
            # Raising here prevents further operations if key is truly missing
            raise ValueError("Korean Tourism API key must be provided and valid.")

        # Initialize language service using the stored default
        self.language = self.default_language
        if self.language not in LANGUAGE_SERVICE_MAP:
            self.logger.warning(
                f"Unsupported language: {self.language}. Falling back to English."
            )
            self.language = "en"

        self.service_name = LANGUAGE_SERVICE_MAP[self.language]
        self.full_base_url = f"{self.BASE_URL}/{self.service_name}"

        # Initialize cache with configured TTL
        self._cache = TTLCache(maxsize=1000, ttl=self._cache_ttl)

        # Initialize semaphore with configured concurrency limit
        self._request_semaphore = asyncio.Semaphore(self._concurrency_limit)

        self._is_fully_initialized = True

    @property
    def cache(self) -> TTLCache:
        """Lazy cache initialization"""
        self._ensure_full_initialization()
        return self._cache  # type: ignore  # We know it's initialized after _ensure_full_initialization

    @classmethod
    async def get_shared_client(cls) -> httpx.AsyncClient:
        """Get or create the shared HTTP client with connection pooling"""
        async with cls._client_lock:
            if cls._shared_client is None:
                cls._shared_client = httpx.AsyncClient(
                    limits=httpx.Limits(
                        max_connections=100, max_keepalive_connections=20
                    ),
                    timeout=httpx.Timeout(30.0),
                )
            return cls._shared_client

    @classmethod
    async def close_all_connections(cls):
        """Close the shared client connection - call this when your application is shutting down"""
        async with cls._client_lock:
            if cls._shared_client is not None:
                await cls._shared_client.aclose()
                cls._shared_client = None

    def _process_response_error(self, response: httpx.Response):
        """Process HTTP errors and raise appropriate exceptions"""
        # Check status code first for efficiency
        if response.status_code >= 400:
            status_code = response.status_code
            request = response.request  # Get the request object

            # Try to extract error message
            error_msg = f"Tourism API error: HTTP {status_code}"
            try:
                error_data = response.json()
                if isinstance(error_data, dict) and "error" in error_data:
                    error_msg = f"Tourism API error: {error_data['error']}"
            except Exception:
                pass

            # Map status code to exception type
            if status_code >= 400 and status_code < 500:
                # Pass response and request to the exception
                raise TourismApiClientError(
                    f"Client error: {error_msg}", response=response, request=request
                )
            else:
                # Pass response and request to the exception
                raise TourismApiServerError(
                    f"Server error: {error_msg}", response=response, request=request
                )

    def _get_cache_key(
        self, endpoint: str, params: Dict[str, Any], language: str
    ) -> str:
        """Generate a unique cache key for an API request, including language."""
        # Sort params to ensure consistent keys, exclude common/dynamic ones
        sorted_params = sorted(
            [
                (k, v)
                for k, v in params.items()
                if k not in ("MobileOS", "MobileApp", "serviceKey", "_type")
            ]
        )
        # Include language in cache key
        param_str = f"lang={language}&" + "&".join(
            [f"{k}={v}" for k, v in sorted_params]
        )
        return f"{endpoint}?{param_str}"

    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=1, max=10),
        retry=retry_if_exception_type(
            (httpx.ConnectTimeout, httpx.ConnectError, TourismApiServerError)
        ),
        reraise=True,
    )
    async def _make_request(
        self,
        endpoint: str,
        params: Dict[str, Any],
        use_cache: bool = True,
        language_override: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Make a request to the API with caching, rate limiting, and language override."""
        # Ensure all initialization is completed
        self._ensure_full_initialization()
        # After initialization, these are guaranteed to be not None
        assert self._request_semaphore is not None

        # Determine the language and base URL for this specific request
        request_language = self.language or "en"  # Fallback to English if None
        if language_override:
            lang_lower = language_override.lower()
            if lang_lower in LANGUAGE_SERVICE_MAP:
                request_language = lang_lower

        request_service_name = LANGUAGE_SERVICE_MAP[request_language]
        request_full_base_url = f"{self.BASE_URL}/{request_service_name}"

        # Check cache first if caching is enabled, using the request-specific language
        if use_cache:
            cache_key = self._get_cache_key(endpoint, params, request_language)
            cached_response = self.cache.get(cache_key)
            if cached_response:
                return cached_response

        # Apply rate limiting dynamically here using instance attributes
        @sleep_and_retry
        @limits(calls=self._rate_limit_calls, period=self._rate_limit_period)
        async def rate_limited_request() -> Dict[str, Any]:
            # Use concurrency semaphore to limit simultaneous requests
            semaphore = self._request_semaphore
            assert semaphore is not None, "Semaphore should be initialized"
            async with semaphore:
                # Add common parameters using constants
                full_params = {
                    "MobileOS": self.MOBILE_OS,
                    "MobileApp": self.MOBILE_APP,
                    # Defaults like numOfRows/pageNo are better set by calling methods or API defaults
                    "_type": self.RESPONSE_FORMAT,
                    **params,
                }

                # API key handling remains the same for now
                serviceKey = (
                    self.api_key
                )  # Already validated in _ensure_full_initialization

                # Build the full URL with the determined service for this request
                url = f"{request_full_base_url}{endpoint}"

                client = await self.get_shared_client()

                # First, encode the parameters
                encoded_params = urllib.parse.urlencode(full_params)

                # Then append the already-encoded service key
                full_url = f"{url}?serviceKey={serviceKey}&{encoded_params}"
                response = await client.get(full_url)

                self._process_response_error(response)

                # Parse the response with better error handling
                try:
                    # Check if the response has content
                    if not response.content or len(response.content.strip()) == 0:
                        raise TourismApiError(
                            "Empty response received from tourism API"
                        )

                    result = response.json()
                except json.JSONDecodeError as e:
                    raise TourismApiError(f"Invalid JSON response: {str(e)}")

                # Extract the items from the nested response structure
                try:
                    response_header = result["response"]["header"]
                    response_body = result["response"]["body"]

                    result_code = response_header.get("resultCode")
                    if result_code != "0000":
                        raise TourismApiError(
                            f"API error: {response_header.get('resultMsg', 'Unknown error')}"
                        )

                    total_count = response_body.get("totalCount", 0)
                    items = []

                    if total_count > 0:
                        items_container = response_body.get("items", {})
                        if "item" in items_container:
                            items = items_container["item"]
                            if not isinstance(items, list):
                                items = [
                                    items
                                ]  # Ensure items is a list even if there's only one result

                    # Structure the results
                    result_data = {
                        "total_count": total_count,
                        "num_of_rows": response_body.get("numOfRows", 0),
                        "page_no": response_body.get("pageNo", 1),
                        "items": items,
                    }

                    # Apply unicode decoding to handle Korean character encoding issues
                    result_data = decode_unicode_escapes(result_data)

                    # Cache the response if caching is enabled, using the request-specific language
                    if use_cache:
                        cache_key = self._get_cache_key(
                            endpoint, params, request_language
                        )
                        self.cache[cache_key] = result_data

                    return result_data

                except (KeyError, TypeError) as e:
                    raise TourismApiError(f"Failed to parse API response: {e}")

        # Call the inner function that has the decorators applied
        result = rate_limited_request()
        return await result  # type: ignore[misc]

    async def search_by_keyword(
        self,
        keyword: str,
        content_type_id: Optional[str] = None,
        area_code: Optional[str] = None,
        sigungu_code: Optional[str] = None,
        cat1: Optional[str] = None,
        cat2: Optional[str] = None,
        cat3: Optional[str] = None,
        language: Optional[str] = None,
        page: int = 1,
        rows: int = 20,
    ) -> Dict[str, Any]:
        """
        Search tourism information by keyword.
        /searchKeyword1

        Args:
            keyword: Search keyword (required)
            content_type_id: Content type ID to filter results
            area_code: Area code to filter results
            sigungu_code: Sigungu code to filter results, areaCode is required
            cat1: Major category code
            cat2: Middle category code (requires cat1)
            cat3: Minor category code (requires cat1 and cat2)
            language: Override the client's default language
            page: Page number for pagination
            rows: Number of items per page

        Returns:
            Dictionary containing search results with structure:
            {
                "total_count": int,     # Total number of matching items
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List of tourism items
                    {
                        "title": str,           # Name of the attraction/place
                        "addr1": str,           # Primary address
                        "addr2": str,           # Secondary address
                        "areacode": str,        # Area code
                        "sigungucode": str,     # Sigungu code
                        "cat1": str,            # Category 1 code
                        "cat2": str,            # Category 2 code
                        "cat3": str,            # Category 3 code
                        "contentid": str,       # Unique content ID
                        "contenttypeid": str,   # Content type ID
                        "createdtime": str,     # Creation timestamp
                        "modifiedtime": str,    # Last modified timestamp
                        "tel": str,             # Phone number
                        "firstimage": str,      # URL of main image
                        "firstimage2": str,     # URL of thumbnail image
                        "mapx": str,            # Longitude
                        "mapy": str,            # Latitude
                        "mlevel": str,          # Map level
                        "cpyrhtDivCd": str      # Copyright division code
                    },
                    # ... more items
                ]
            }
        """
        if not keyword:
            raise ValueError("Keyword must be provided for search_by_keyword")

        params: Dict[str, Any] = {
            "keyword": keyword,
            "arrange": self.ARRANGE_MODIFIED_WITH_IMAGE,
            "pageNo": str(page),
            "numOfRows": str(rows),
        }

        if content_type_id:
            params["contentTypeId"] = content_type_id

        if area_code:
            params["areaCode"] = area_code

            if sigungu_code:
                params["sigunguCode"] = sigungu_code

        if cat1:
            params["cat1"] = cat1

            if cat2:
                params["cat2"] = cat2

                if cat3:
                    params["cat3"] = cat3

        # Pass language override directly to _make_request
        return await self._make_request(
            self.SEARCH_KEYWORD_ENDPOINT, params, language_override=language
        )

    async def get_area_based_list(
        self,
        area_code: Optional[str] = None,
        content_type_id: Optional[str] = None,
        sigunguCode: Optional[str] = None,
        cat1: Optional[str] = None,
        cat2: Optional[str] = None,
        cat3: Optional[str] = None,
        language: Optional[str] = None,
        page: int = 1,
        rows: int = 20,
    ) -> Dict[str, Any]:
        """
        Get a list of tourism information by area.

        Args:
            area_code: Area code to filter results
            content_type_id: Content type ID to filter results
            cat1: Major category code
            cat2: Middle category code (requires cat1)
            cat3: Minor category code (requires cat1 and cat2)
            language: Override the client's default language
            page: Page number for pagination
            rows: Number of items per page
            sigunguCode: Sigungu code to filter results, areaCode is required
        Returns:
            Dictionary containing area-based tourism information with structure:
            {
                "total_count": int,     # Total number of matching items
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List of tourism items
                    {
                        "title": str,           # Name of the attraction/place
                        "addr1": str,           # Primary address
                        "addr2": str,           # Secondary address
                        "areacode": str,        # Area code
                        "sigungucode": str,     # Sigungu code
                        "cat1": str,            # Category 1 code
                        "cat2": str,            # Category 2 code
                        "cat3": str,            # Category 3 code
                        "contentid": str,       # Unique content ID
                        "contenttypeid": str,   # Content type ID
                        "createdtime": str,     # Creation timestamp
                        "modifiedtime": str,    # Last modified timestamp
                        "tel": str,             # Phone number
                        "firstimage": str,      # URL of main image
                        "firstimage2": str,     # URL of thumbnail image
                        "mapx": str,            # Longitude
                        "mapy": str,            # Latitude
                        "zipcode": str,         # Postal code
                        "mlevel": str           # Map level
                    },
                    # ... more items
                ]
            }
        """
        params: Dict[str, Any] = {
            "arrange": self.ARRANGE_MODIFIED_WITH_IMAGE,
            "pageNo": str(page),
            "numOfRows": str(rows),
            # "_type": self.RESPONSE_FORMAT, # Added in _make_request
        }

        # Add optional filters
        if area_code:
            params["areaCode"] = area_code
            if sigunguCode:
                params["sigunguCode"] = sigunguCode

        if content_type_id:
            params["contentTypeId"] = content_type_id

        # Add category filters if provided
        if cat1:
            params["cat1"] = cat1

            if cat2:
                params["cat2"] = cat2

                if cat3:
                    params["cat3"] = cat3

        # Pass language override directly to _make_request
        return await self._make_request(
            self.AREA_BASED_LIST_ENDPOINT, params, language_override=language
        )

    async def get_location_based_list(
        self,
        mapx: float,
        mapy: float,
        radius: int,
        content_type_id: Optional[str] = None,
        language: Optional[str] = None,
        page: int = 1,
        rows: int = 20,
    ) -> Dict[str, Any]:
        """
        Get a list of tourism information by location.

        Args:
            mapx: Map X coordinate
            mapy: Map Y coordinate
            radius: Radius in meters
            content_type_id: Content type ID from the tourism API
            language: Override the client's default language
            page: Page number for pagination
            rows: Number of items per page

        Returns:
            Dictionary containing location-based tourism information with structure:
            {
                "total_count": int,     # Total number of matching items
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List of tourism items
                    {
                        "title": str,           # Name of the attraction/place
                        "addr1": str,           # Primary address
                        "addr2": str,           # Secondary address
                        "areacode": str,        # Area code
                        "sigungucode": str,     # Sigungu code
                        "cat1": str,            # Category 1 code
                        "cat2": str,            # Category 2 code
                        "cat3": str,            # Category 3 code
                        "contentid": str,       # Unique content ID
                        "contenttypeid": str,   # Content type ID
                        "createdtime": str,     # Creation timestamp
                        "modifiedtime": str,    # Last modified timestamp
                        "tel": str,             # Phone number
                        "firstimage": str,      # URL of main image
                        "firstimage2": str,     # URL of thumbnail image
                        "mapx": str,            # Longitude
                        "mapy": str,            # Latitude
                        "mlevel": str,          # Map level
                        "dist": str             # Distance from the specified coordinates
                    },
                    # ... more items
                ]
            }
        """
        # Parameter validation
        if mapx is None or mapy is None or radius is None:
            raise ValueError(
                "mapx, mapy, and radius are required for location-based search"
            )
        try:
            # Ensure radius is a positive integer
            radius_int = int(radius)
            if radius_int <= 0:
                raise ValueError("radius must be a positive integer")
        except ValueError:
            raise ValueError("radius must be a valid integer")

        params: Dict[str, Any] = {
            "pageNo": str(page),
            "numOfRows": str(rows),
            "arrange": self.ARRANGE_MODIFIED_WITH_IMAGE,
            "mapX": str(mapx),
            "mapY": str(mapy),
            "radius": str(radius_int),
        }

        if content_type_id:
            params["contentTypeId"] = content_type_id

        # Pass language override directly to _make_request
        return await self._make_request(
            self.LOCATION_BASED_LIST_ENDPOINT, params, language_override=language
        )

    async def search_festival(
        self,
        event_start_date: str,
        event_end_date: Optional[str] = None,
        area_code: Optional[str] = None,
        sigungu_code: Optional[str] = None,
        language: Optional[str] = None,
        page: int = 1,
        rows: int = 20,
    ) -> Dict[str, Any]:
        """
        Search for festivals by date and location.

        Args:
            event_start_date: Start date of the event (YYYYMMDD)
            event_end_date: End date of the event (YYYYMMDD)
            area_code: Area code to filter results
            sigungu_code: Sigungu code to filter results, areaCode is required
            language: Override the client's default language
            page: Page number for pagination
            rows: Number of items per page

        Returns:
            Dictionary containing festival information with structure:
            {
                "total_count": int,     # Total number of matching items
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List of festival items
                    {
                        "title": str,           # Name of the festival
                        "addr1": str,           # Primary address
                        "addr2": str,           # Secondary address
                        "areacode": str,        # Area code
                        "contentid": str,       # Unique content ID
                        "contenttypeid": str,   # Content type ID
                        "createdtime": str,     # Creation timestamp
                        "eventstartdate": str,  # Festival start date
                        "eventenddate": str,    # Festival end date
                        "firstimage": str,      # URL of main image
                        "firstimage2": str,     # URL of thumbnail image
                        "mapx": str,            # Longitude
                        "mapy": str,            # Latitude
                        "mlevel": str,          # Map level
                        "tel": str,             # Phone number
                        "cat1": str,            # Category 1 code
                        "cat2": str,            # Category 2 code
                        "cat3": str             # Category 3 code
                    },
                    # ... more items
                ]
            }
        """
        if not event_start_date:
            raise ValueError("event_start_date (YYYYMMDD) is required")
        # Basic format check (can be improved with regex)
        if not (
            isinstance(event_start_date, str)
            and len(event_start_date) == 8
            and event_start_date.isdigit()
        ):
            raise ValueError("event_start_date must be in YYYYMMDD format")
        if event_end_date and not (
            isinstance(event_end_date, str)
            and len(event_end_date) == 8
            and event_end_date.isdigit()
        ):
            raise ValueError("event_end_date must be in YYYYMMDD format")

        params: Dict[str, Any] = {
            "eventStartDate": event_start_date,
            "pageNo": str(page),
            "numOfRows": str(rows),
            "arrange": self.ARRANGE_MODIFIED_WITH_IMAGE,  # Default arrange added
        }

        if event_end_date:
            params["eventEndDate"] = event_end_date

        if area_code:
            params["areaCode"] = area_code

            if sigungu_code:
                params["sigunguCode"] = sigungu_code

        # Pass language override directly to _make_request
        return await self._make_request(
            self.SEARCH_FESTIVAL_ENDPOINT, params, language_override=language
        )

    async def search_stay(
        self,
        area_code: Optional[str] = None,
        sigungu_code: Optional[str] = None,
        rows: int = 20,
        page: int = 1,
        language: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Search for stays by area and sigungu.

        Args:
            area_code: Area code to filter results
            sigungu_code: Sigungu code to filter results, areaCode is required
            rows: Number of items per page
            page: Page number for pagination
            language: Override the client's default language

        Returns:
            Dictionary containing accommodation information with structure:
            {
                "total_count": int,     # Total number of matching items
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List of accommodation items
                    {
                        "title": str,           # Name of the accommodation
                        "addr1": str,           # Primary address
                        "addr2": str,           # Secondary address
                        "areacode": str,        # Area code
                        "sigungucode": str,     # Sigungu code
                        "contentid": str,       # Unique content ID
                        "contenttypeid": str,   # Content type ID
                        "createdtime": str,     # Creation timestamp
                        "firstimage": str,      # URL of main image
                        "firstimage2": str,     # URL of thumbnail image
                        "mapx": str,            # Longitude
                        "mapy": str,            # Latitude
                        "mlevel": str,          # Map level
                        "tel": str,             # Phone number
                        "cat1": str,            # Category 1 code
                        "cat2": str,            # Category 2 code
                        "cat3": str,            # Category 3 code
                        "hanok": str,           # Korean traditional house flag
                        "benikia": str,         # Benikia hotel flag
                        "goodstay": str         # Goodstay accommodation flag
                    },
                    # ... more items
                ]
            }
        """
        params: Dict[str, Any] = {
            "pageNo": str(page),
            "numOfRows": str(rows),
            "arrange": self.ARRANGE_MODIFIED_WITH_IMAGE,  # Default arrange added
        }

        if area_code:
            params["areaCode"] = area_code

            if sigungu_code:
                params["sigunguCode"] = sigungu_code

        # Pass language override directly to _make_request
        return await self._make_request(
            self.SEARCH_STAY_ENDPOINT, params, language_override=language
        )

    async def get_detail_common(
        self,
        content_id: str,
        language: Optional[str] = None,
        rows: int = 20,
        page: int = 1,
    ) -> Dict[str, Any]:
        """
        Get common information by type basic information, schematic image,
        representative image, classification information, regional information,
        address information, coordinate information, outline information, road guidance information,
        image information, linked tourism information list

        Args:
            content_id: Content ID from the tourism API
            language: Override the client's default language
            rows: Number of items per page
            page: Page number for pagination

        Returns:
            Dictionary containing common details about a tourism item with structure:
            {
                "total_count": int,     # Total number of matching items (typically 1)
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List containing a single item's details
                    {
                        "title": str,           # Name of the item
                        "contentid": str,       # Unique content ID
                        "contenttypeid": str,   # Content type ID
                        "addr1": str,           # Primary address
                        "addr2": str,           # Secondary address
                        "areacode": str,        # Area code
                        "sigungucode": str,     # Sigungu code
                        "cat1": str,            # Category 1 code
                        "cat2": str,            # Category 2 code
                        "cat3": str,            # Category 3 code
                        "mapx": str,            # Longitude
                        "mapy": str,            # Latitude
                        "mlevel": str,          # Map level
                        "overview": str,        # Detailed description
                        "tel": str,             # Phone number
                        "telname": str,         # Contact name
                        "homepage": str,        # Website URL
                        "firstimage": str,      # URL of main image
                        "firstimage2": str,     # URL of thumbnail image
                        "createdtime": str,     # Creation timestamp
                        "modifiedtime": str,    # Last modified timestamp
                        "zipcode": str          # Postal code
                    }
                ]
            }
        """
        if not content_id:
            raise ValueError("content_id is required")

        params: Dict[str, Any] = {
            "contentId": content_id,
            "pageNo": str(page),
            "numOfRows": str(rows),
        }

        # Pass language override directly to _make_request
        return await self._make_request(
            self.DETAIL_COMMON_ENDPOINT, params, language_override=language
        )

    async def get_detail_images(
        self,
        content_id: str,
        language: Optional[str] = None,
        rows: int = 20,
        page: int = 1,
    ) -> Dict[str, Any]:
        """
        Get images for a tourism item.

        Args:
            content_id: Content ID from the tourism API
            language: Override the client's default language
            rows: Number of items per page
            page: Page number for pagination

        Returns:
            Dictionary containing images for a tourism item with structure:
            {
                "total_count": int,     # Total number of matching items
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List of image items
                    {
                        "contentid": str,       # Content ID this image belongs to
                        "imgname": str,         # Image name
                        "originimgurl": str,    # URL of original image
                        "smallimageurl": str,   # URL of small/thumbnail image
                        "serialnum": str,       # Serial number
                        "cpyrhtDivCd": str      # Copyright division code
                    },
                    # ... more items
                ]
            }
        """
        if not content_id:
            raise ValueError("content_id is required")

        params: Dict[str, Any] = {
            "contentId": content_id,
            "numOfRows": str(rows),
            "pageNo": str(page),
        }

        # Pass language override directly to _make_request
        return await self._make_request(
            self.DETAIL_IMAGE_ENDPOINT, params, language_override=language
        )

    async def get_detail_intro(
        self,
        content_id: str,
        content_type_id: str,
        language: Optional[str] = None,
        rows: int = 20,
        page: int = 1,
    ) -> Dict[str, Any]:
        """
        Function to check detailed introduction (off day, opening period, etc.)

        Args:
            content_id: Content ID from the tourism API
            content_type_id: Content type ID from the tourism API
            language: Override the client's default language
            rows: Number of items per page
            page: Page number for pagination

        Returns:
            Dictionary containing detailed introduction information with structure:
            {
                "total_count": int,     # Total number of matching items
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List containing a single item's intro details
                    {
                        "contentid": str,         # Content ID
                        "contenttypeid": str,     # Content type ID
                        # The following fields vary based on content_type_id:

                        # Attraction specific fields may include:
                        "infocenter": str,        # Information center
                        "restdate": str,          # Rest/closing days
                        "usetime": str,           # Hours of operation
                        "parking": str,           # Parking information
                        "chkbabycarriage": str,   # Baby carriage accessibility
                        "chkpet": str,            # Pet allowance
                        "chkcreditcard": str,     # Credit card acceptance

                        # Festival specific fields may include:
                        "eventstartdate": str,    # Festival start date
                        "eventenddate": str,      # Festival end date
                        "eventplace": str,        # Festival venue
                        "usetimefestival": str,   # Festival hours
                        "sponsor1": str,          # Primary sponsor
                        "sponsor2": str,          # Secondary sponsor

                        # Restaurant specific fields may include:
                        "firstmenu": str,         # Main menu items
                        "treatmenu": str,         # Specialty dishes
                        "opentimefood": str,      # Opening hours
                        "restdatefood": str,      # Closing days
                        "reservationfood": str,   # Reservation information

                        # Accommodation specific fields may include:
                        "checkintime": str,       # Check-in time
                        "checkouttime": str,      # Check-out time
                        "roomcount": str,         # Number of rooms
                        "reservationurl": str,    # Reservation website
                        "benikia": str,           # Benikia certification
                        "goodstay": str           # Goodstay certification
                    }
                ]
            }

            Note: The actual fields returned depend on the content_type_id and will vary between different types of tourism items.
        """
        if not content_id:
            raise ValueError("content_id is required")
        if not content_type_id:
            raise ValueError("content_type_id is required")
        params: Dict[str, Any] = {
            "contentId": content_id,
            "contentTypeId": content_type_id,
            "numOfRows": str(rows),
            "pageNo": str(page),
        }

        # Pass language override directly to _make_request
        return await self._make_request(
            self.DETAIL_INTRO_ENDPOINT, params, language_override=language
        )

    async def get_detail_info(
        self,
        content_id: str,
        content_type_id: str,
        language: Optional[str] = None,
        rows: int = 20,
        page: int = 1,
    ) -> Dict[str, Any]:
        """
        Check the details of additional tourism information.

        Args:
            content_id: Content ID from the tourism API
            content_type_id: Content type ID from the tourism API
            language: Override the client's default language
            rows: Number of items per page
            page: Page number for pagination

        Returns:
            Dictionary containing additional detailed information with structure:
            {
                "total_count": int,     # Total number of matching items
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List of additional info items
                    {
                        "contentid": str,       # Content ID this info belongs to
                        "contenttypeid": str,   # Content type ID
                        "infoname": str,        # Name of the information
                        "infotext": str,        # Detailed text information
                        "fldgubun": str,        # Field division code
                        "serialnum": str        # Serial number
                    },
                    # ... more items
                ]
            }

            Note: Each item in the 'items' list represents a specific piece of additional information about the tourism item.
        """
        if not content_id:
            raise ValueError("content_id is required")
        if not content_type_id:
            raise ValueError("content_type_id is required")

        params: Dict[str, Any] = {
            "contentId": content_id,
            "contentTypeId": content_type_id,
            "numOfRows": str(rows),
            "pageNo": str(page),
        }

        # Pass language override directly to _make_request
        return await self._make_request(
            self.DETAIL_INFO_ENDPOINT, params, language_override=language
        )

    async def get_area_based_sync_list(
        self,
        content_type_id: Optional[str] = None,
        area_code: Optional[str] = None,
        sigungu_code: Optional[str] = None,
        cat1: Optional[str] = None,
        cat2: Optional[str] = None,
        cat3: Optional[str] = None,
        language: Optional[str] = None,
        show_flag: Optional[Literal["0", "1"]] = None,
        rows: int = 20,
        page: int = 1,
    ) -> Dict[str, Any]:
        """
        Detailed function of inquiring about the tourism information synchronization list (provided whether the contents are displayed or not)

        Args:
            area_code: Area code from the tourism API
            content_type_id: Content type ID from the tourism API
            sigungu_code: Sigungu code from the tourism API
            cat1: Category 1 from the tourism API
            cat2: Category 2 from the tourism API
            cat3: Category 3 from the tourism API
            language: Override the client's default language
            show_flag: Show flag from the tourism API
            rows: Number of items per page
            page: Page number for pagination

        Returns:
            Dictionary containing synchronized tourism information with structure:
            {
                "total_count": int,     # Total number of matching items
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List of tourism items with sync status
                    {
                        "title": str,           # Name of the attraction/place
                        "addr1": str,           # Primary address
                        "addr2": str,           # Secondary address
                        "areacode": str,        # Area code
                        "sigungucode": str,     # Sigungu code
                        "cat1": str,            # Category 1 code
                        "cat2": str,            # Category 2 code
                        "cat3": str,            # Category 3 code
                        "contentid": str,       # Unique content ID
                        "contenttypeid": str,   # Content type ID
                        "createdtime": str,     # Creation timestamp
                        "modifiedtime": str,    # Last modified timestamp
                        "tel": str,             # Phone number
                        "firstimage": str,      # URL of main image
                        "firstimage2": str,     # URL of thumbnail image
                        "mapx": str,            # Longitude
                        "mapy": str,            # Latitude
                        "mlevel": str,          # Map level
                        "showflag": str         # Display status flag
                    },
                    # ... more items
                ]
            }
        """
        params: Dict[str, Any] = {
            "numOfRows": str(rows),  # Use str()
            "pageNo": str(page),  # Use str()
            "arrange": self.ARRANGE_MODIFIED_WITH_IMAGE,
        }
        if show_flag:
            params["showFlag"] = show_flag

        if area_code:
            params["areaCode"] = area_code

            if sigungu_code:
                params["sigunguCode"] = sigungu_code

        if cat1:
            params["cat1"] = cat1

            if cat2:
                params["cat2"] = cat2

                if cat3:
                    params["cat3"] = cat3

        if content_type_id:
            params["contentTypeId"] = content_type_id

        # Pass language override directly to _make_request
        return await self._make_request(
            self.AREA_BASED_SYNC_LIST_ENDPOINT, params, language_override=language
        )

    async def get_area_code_list(
        self,
        area_code: Optional[str] = None,
        language: Optional[str] = None,
        rows: int = 20,
        page: int = 1,
    ) -> Dict[str, Any]:
        """
        Get the list of area codes.

        Args:
            area_code: Area code from the tourism API
            language: Override the client's default language
            rows: Number of items per page
            page: Page number for pagination

        Returns:
            Dictionary containing area code information with structure:
            {
                "total_count": int,     # Total number of matching items
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List of area code items
                    {
                        "code": str,            # Area code value
                        "name": str,            # Area name
                        "rnum": str             # Row number
                    },
                    # ... more items
                ]
            }

            If area_code is provided, returns sigungu codes for that area.
            If area_code is not provided, returns top-level area codes.
        """
        params: Dict[str, Any] = {
            "numOfRows": str(rows),  # Use str()
            "pageNo": str(page),  # Use str()
        }
        if area_code:
            params["areaCode"] = area_code

        # Pass language override directly to _make_request
        return await self._make_request(
            self.AREA_CODE_LIST_ENDPOINT, params, language_override=language
        )

    async def get_category_code_list(
        self,
        content_type_id: Optional[str] = None,
        language: Optional[str] = None,
        cat1: Optional[str] = None,
        cat2: Optional[str] = None,
        cat3: Optional[str] = None,
        rows: int = 20,
        page: int = 1,
    ) -> Dict[str, Any]:
        """
        Get the list of category codes.

        Args:
            content_type_id: Content type ID from the tourism API
            language: Override the client's default language
            cat1: Category 1 from the tourism API
            cat2: Category 2 from the tourism API
            cat3: Category 3 from the tourism API
            rows: Number of items per page
            page: Page number for pagination

        Returns:
            Dictionary containing category code information with structure:
            {
                "total_count": int,     # Total number of matching items
                "num_of_rows": int,     # Number of items per page
                "page_no": int,         # Current page number
                "items": [              # List of category code items
                    {
                        "code": str,            # Category code value
                        "name": str,            # Category name
                        "rnum": str             # Row number
                    },
                    # ... more items
                ]
            }

            The categories returned depend on the parameters provided:
            - Without any parameters: Returns top-level categories (cat1)
            - With cat1: Returns subcategories (cat2) under that cat1
            - With cat1 and cat2: Returns subcategories (cat3) under that cat2
        """
        params: Dict[str, Any] = {
            "numOfRows": str(rows),  # Use str()
            "pageNo": str(page),  # Use str()
        }

        if content_type_id:
            params["contentTypeId"] = content_type_id

        if cat1:
            params["cat1"] = cat1

            if cat2:
                params["cat2"] = cat2

                if cat3:
                    params["cat3"] = cat3

        # Pass language override directly to _make_request
        return await self._make_request(
            self.CATEGORY_CODE_LIST_ENDPOINT, params, language_override=language
        )


if __name__ == "__main__":
    import os
    import asyncio

    api_key = os.environ.get("KOREA_TOURISM_API_KEY")
    if not api_key:
        raise ValueError("KOREA_TOURISM_API_KEY environment variable is not set")
    client = KoreaTourismApiClient(api_key=api_key)
    print(asyncio.run(client.search_by_keyword(keyword="Gyeongbokgung")))
