"""
This module contains pure functions for parsing data from YouTube's HTML and JSON structures.
"""

import json
import logging
import re
from typing import Optional

from .exceptions import MetadataParsingError, VideoUnavailableError
from .utils import _deep_get

logger = logging.getLogger(__name__)


def find_ytcfg(html: str) -> Optional[dict]:
    """
    Finds and parses the `ytcfg` data from a page's HTML source.

    This data contains important context for making subsequent API requests,
    such as the INNERTUBE_API_KEY and client version.
    """
    match = re.search(r"ytcfg\.set\s*\(\s*({.*?})\s*\)\s*;", html, re.DOTALL)
    if match:
        try:
            return json.loads(match.group(1))
        except json.JSONDecodeError:
            logger.warning("Failed to parse ytcfg JSON.")
            return None
    logger.warning("Could not find ytcfg data in HTML.")
    return None


def extract_and_parse_json(html: str, variable_name: str) -> dict | None:
    """
    Finds a JavaScript variable assignment in HTML and parses its JSON content.

    This is used to extract data blobs like `ytInitialData` or
    `ytInitialPlayerResponse` by locating the variable and then carefully
    finding the matching opening and closing braces of the JSON object.

    Args:
        html: The HTML content of the page.
        variable_name: The name of the JavaScript variable (e.g., "ytInitialData").

    Returns:
        A dictionary parsed from the JSON, or None if not found or invalid.
    """
    try:
        start_key = f"var {variable_name} = "
        start_index = html.find(start_key)

        if start_index == -1:
            logger.warning(f"Could not find JavaScript variable '{variable_name}'.")
            return None

        start_index += len(start_key)

        # Find the opening brace of the JSON object
        first_brace = html.find("{", start_index)
        if first_brace == -1:
            logger.warning(f"Could not find opening brace for '{variable_name}'.")
            return None

        # Find the matching closing brace
        open_braces = 1
        for i, char in enumerate(html[first_brace + 1 :], start=first_brace + 1):
            if char == "{":
                open_braces += 1
            elif char == "}":
                open_braces -= 1

            if open_braces == 0:
                json_str = html[first_brace : i + 1]
                return json.loads(json_str)

        logger.warning(f"Could not find matching closing brace for '{variable_name}'.")
        return None

    except (json.JSONDecodeError, IndexError) as e:
        logger.error(f"Failed to parse JSON for '{variable_name}': {e}")
        return None


def parse_duration(duration_label: str) -> int | None:
    """
    Parses a human-readable duration label into a total number of seconds.

    Example:
        "11 minutes, 6 seconds" -> 666
    """
    if not duration_label:
        return None

    parts = duration_label.split(",")
    total_seconds = 0
    for part in parts:
        part = part.strip()
        if "hour" in part:
            total_seconds += int(part.split(" ")[0]) * 3600
        elif "minute" in part:
            total_seconds += int(part.split(" ")[0]) * 60
        elif "second" in part:
            total_seconds += int(part.split(" ")[0])
    return total_seconds if total_seconds > 0 else None


def parse_view_count(view_count_text: str) -> int | None:
    """
    Parses a view count string (e.g., '2,905,010 views') into an integer.
    """
    if not view_count_text:
        return None
    try:
        return int(view_count_text.split(" ")[0].replace(",", ""))
    except (ValueError, TypeError, IndexError):
        return None


def find_like_count(player_response_data: dict) -> int | None:
    """
    Extracts the like count from the `ytInitialPlayerResponse` data structure.
    """
    try:
        renderer = _deep_get(player_response_data, "microformat.playerMicroformatRenderer", {})
        like_count_str = renderer.get("likeCount")
        if like_count_str and isinstance(like_count_str, str) and like_count_str.isdigit():
            return int(like_count_str)
        return None
    except (KeyError, TypeError):
        logger.debug("Could not find like count in player response data.")
        return None


def find_heatmap(initial_data: dict) -> list | None:
    """
    Finds the video's "Most replayed" heatmap data from the `ytInitialData`.

    This data is located within the `frameworkUpdates` part of the initial data.
    """
    try:
        mutations = _deep_get(initial_data, "frameworkUpdates.entityBatchUpdate.mutations", [])
        for mutation in mutations:
            if "payload" in mutation and "macroMarkersListEntity" in mutation["payload"]:
                markers_list = _deep_get(
                    mutation,
                    "payload.macroMarkersListEntity.markersList.0.value.macroMarkersMarkersListRenderer.contents",
                    [],
                )
                heatmap = []
                for marker in markers_list:
                    if "marker" in marker and "heatmapMarker" in marker["marker"]:
                        heatmap_marker = marker["marker"]["heatmapMarker"]
                        heatmap.append(
                            {
                                "startMillis": _deep_get(
                                    heatmap_marker,
                                    "timeRangeStartMarker.markerDurationFromStartMillis",
                                ),
                                "durationMillis": _deep_get(heatmap_marker, "markerDurationMillis"),
                                "intensityScoreNormalized": _deep_get(heatmap_marker, "intensityScoreNormalized"),
                            }
                        )
                return heatmap
    except (KeyError, TypeError, IndexError):
        logger.debug("Could not find heatmap data in initial data.")
        return None


def extract_videos_from_renderers(renderers: list) -> tuple[list, str | None]:
    """
    Parses a list of video renderers from a channel's "Videos" tab.

    This function iterates through a list of renderer items, extracts the
    relevant information for each video, and finds the continuation token for
    paginating to the next set of results.

    Args:
        renderers: A list of renderer dictionaries from the channel page's data.

    Returns:
        A tuple containing:
        - A list of dictionaries, where each dict is a simplified video object.
        - A continuation token string for the next page, or None if not found.
    """
    videos = []
    continuation_token = None
    if not renderers:
        return videos, continuation_token

    for renderer in renderers:
        if "richItemRenderer" in renderer:
            video_data = _deep_get(renderer, "richItemRenderer.content.videoRenderer")
            if not video_data:
                continue

            badges = _deep_get(video_data, "badges", [])
            overlays = _deep_get(video_data, "thumbnailOverlays", [])
            is_members_only = any(
                _deep_get(b, "metadataBadgeRenderer.style") == "BADGE_STYLE_TYPE_MEMBERS_ONLY" for b in badges
            )
            is_live = any("thumbnailOverlayNowPlayingRenderer" in o for o in overlays)
            is_premiere = any(
                _deep_get(o, "thumbnailOverlayTimeStatusRenderer.text.runs.0.text") == "PREMIERE" for o in overlays
            )
            is_verified = any(
                _deep_get(b, "metadataBadgeRenderer.style") == "BADGE_STYLE_TYPE_VERIFIED"
                for b in _deep_get(video_data, "ownerBadges", [])
            )

            videos.append(
                {
                    "videoId": video_data.get("videoId"),
                    "title": _deep_get(video_data, "title.runs.0.text"),
                    "descriptionSnippet": _deep_get(video_data, "descriptionSnippet.runs.0.text"),
                    "thumbnails": _deep_get(video_data, "thumbnail.thumbnails", []),
                    "publishedTimeText": _deep_get(video_data, "publishedTimeText.simpleText"),
                    "lengthSeconds": parse_duration(
                        _deep_get(
                            video_data,
                            "lengthText.accessibility.accessibilityData.label",
                        )
                    ),
                    "viewCount": parse_view_count(_deep_get(video_data, "viewCountText.simpleText")),
                    "watchUrl": f"https://www.youtube.com{_deep_get(video_data, 'navigationEndpoint.commandMetadata.webCommandMetadata.url')}",  # noqa: E501
                    "isLive": is_live,
                    "isPremiere": is_premiere,
                    "isMembersOnly": is_members_only,
                    "isVerified": is_verified,
                }
            )

        if "continuationItemRenderer" in renderer:
            continuation_token = _deep_get(
                renderer,
                "continuationItemRenderer.continuationEndpoint.continuationCommand.token",
            )

    return videos, continuation_token


def extract_videos_from_playlist_renderer(renderer: dict) -> tuple[list, str | None]:
    """
    Parses a `playlistVideoListRenderer` from a playlist page.

    This function iterates through the contents of the renderer, extracts
    video data, and finds the continuation token for pagination.

    Args:
        renderer: A `playlistVideoListRenderer` dictionary from the page's data.

    Returns:
        A tuple containing:
        - A list of dictionaries, where each dict is a simplified video object.
        - A continuation token string for the next page, or None if not found.
    """
    videos = []
    continuation_token = None
    if not renderer or "contents" not in renderer:
        return videos, continuation_token

    renderer_list = renderer["contents"]

    for item in renderer_list:
        if "playlistVideoRenderer" in item:
            videos.append(parse_video_renderer(item["playlistVideoRenderer"]))
        elif "continuationItemRenderer" in item:
            continuation_endpoint = _deep_get(item, "continuationItemRenderer.continuationEndpoint")
            if continuation_endpoint and "continuationCommand" in continuation_endpoint:
                continuation_token = _deep_get(continuation_endpoint, "continuationCommand.token")
            # Fallback for the case where it's nested deeper
            elif continuation_endpoint and "commandExecutorCommand" in continuation_endpoint:
                for command in _deep_get(continuation_endpoint, "commandExecutorCommand.commands", []):
                    if "continuationCommand" in command:
                        continuation_token = _deep_get(command, "continuationCommand.token")
                        break

    return videos, continuation_token


def parse_video_renderer(renderer: dict) -> dict:
    """
    Parses a single `playlistVideoRenderer` from a playlist page.
    """
    published_time_text = None
    video_info_runs = _deep_get(renderer, "videoInfo.runs", [])
    if video_info_runs:
        # The text is usually in the last run, like "6 years ago"
        text = video_info_runs[-1].get("text")
        if text:
            published_time_text = text

    return {
        "videoId": renderer.get("videoId"),
        "title": _deep_get(renderer, "title.runs.0.text"),
        "thumbnails": _deep_get(renderer, "thumbnail.thumbnails", []),
        "publishedTimeText": published_time_text,
        "lengthSeconds": parse_duration(_deep_get(renderer, "lengthText.accessibility.accessibilityData.label")),
        "watchUrl": f"https://www.youtube.com{_deep_get(renderer, 'navigationEndpoint.commandMetadata.webCommandMetadata.url')}",
    }


def parse_channel_metadata(initial_data: dict) -> dict:
    """
    Parses the main metadata for a channel from the `ytInitialData` object.
    """
    metadata_renderer = _deep_get(initial_data, "metadata.channelMetadataRenderer")
    if not metadata_renderer:
        logger.warning("Could not find channelMetadataRenderer in page data.")
        raise MetadataParsingError("Could not find channelMetadataRenderer in page data.")

    vanity_url_path = (
        "contents.twoColumnBrowseResultsRenderer.tabs.0.tabRenderer.endpoint.browseEndpoint.canonicalBaseUrl"  # noqa: E501
    )
    vanity_handle = _deep_get(initial_data, vanity_url_path)
    vanity_url = (
        f"https://www.youtube.com{vanity_handle}" if vanity_handle else metadata_renderer.get("vanityChannelUrl")
    )

    return {
        "title": metadata_renderer.get("title"),
        "description": metadata_renderer.get("description"),
        "channel_id": metadata_renderer.get("externalId"),
        "vanity_url": vanity_url,
        "keywords": [kw.strip() for kw in metadata_renderer.get("keywords", "").split(",") if kw.strip()],
        "is_family_safe": metadata_renderer.get("isFamilySafe"),
    }


def parse_playlist_metadata(initial_data: dict) -> dict:
    """
    Parses the main metadata for a playlist from the `ytInitialData` object.
    """
    header = _deep_get(initial_data, "header.playlistHeaderRenderer")
    microformat = _deep_get(initial_data, "microformat.microformatDataRenderer")
    sidebar_primary = _deep_get(
        initial_data, "sidebar.playlistSidebarRenderer.items.0.playlistSidebarPrimaryInfoRenderer"
    )
    sidebar_secondary = _deep_get(
        initial_data, "sidebar.playlistSidebarRenderer.items.1.playlistSidebarSecondaryInfoRenderer"
    )

    video_count_text = _deep_get(sidebar_primary, "stats.0.runs.0.text", "").replace(",", "")
    playlist_id = None
    if microformat and "urlCanonical" in microformat:
        match = re.search(r"list=([^&]+)", microformat["urlCanonical"])
        if match:
            playlist_id = match.group(1)

    author = _deep_get(sidebar_secondary, "videoOwner.videoOwnerRenderer.title.runs.0.text")
    if not author:
        author = _deep_get(header, "ownerText.runs.0.text")

    return {
        "title": _deep_get(microformat, "title"),
        "author": author,
        "description": _deep_get(microformat, "description"),
        "video_count": int(video_count_text) if video_count_text.isdigit() else 0,
        "playlist_id": playlist_id,
    }


def parse_video_metadata(player_response_data: dict, initial_data: dict) -> dict:
    """
    Parses comprehensive video metadata from the `ytInitialPlayerResponse`
    and `ytInitialData` objects from a video's watch page.
    """
    if not player_response_data and not initial_data:
        logger.error("Could not extract playerResponse or initialData from page.")
        raise VideoUnavailableError("Could not extract playerResponse or initialData from page.")

    video_details = _deep_get(player_response_data, "videoDetails", {})
    microformat = _deep_get(player_response_data, "microformat.playerMicroformatRenderer", {})

    return {
        "video_id": video_details.get("videoId"),
        "title": video_details.get("title"),
        "channel_name": video_details.get("author"),
        "channel_id": video_details.get("channelId"),
        "duration_seconds": int(video_details.get("lengthSeconds", 0)),
        "view_count": int(video_details.get("viewCount", 0)),
        "publish_date": microformat.get("publishDate"),
        "upload_date": microformat.get("uploadDate"),
        "category": microformat.get("category"),
        "like_count": find_like_count(player_response_data),
        "keywords": video_details.get("keywords", []),
        "thumbnails": _deep_get(video_details, "thumbnail.thumbnails", []),
        "is_live": video_details.get("isLiveContent", False),
        "full_description": video_details.get("shortDescription"),
        "heatmap": find_heatmap(initial_data),
        "subscriber_count_text": _deep_get(
            initial_data,
            "contents.twoColumnWatchNextResults.results.results.contents.1.videoSecondaryInfoRenderer.owner.videoOwnerRenderer.subscriberCountText.simpleText",
        ),
    }
