"""Implements known blink API calls."""

import json
import logging
import string
from json import dumps
from asyncio import sleep
from urllib.parse import urlencode, urlparse, parse_qs
from blinkpy.helpers.util import (
    get_time,
    Throttle,
    local_storage_clip_url_template,
)
from blinkpy.helpers.oauth_parser import OAuthArgsParser
from blinkpy.helpers.constants import (
    TIMEOUT,
    DEFAULT_USER_AGENT,
    OAUTH_CLIENT_ID,
    OAUTH_GRANT_TYPE_PASSWORD,
    OAUTH_GRANT_TYPE_REFRESH_TOKEN,
    OAUTH_SCOPE,
    OAUTH_AUTHORIZE_URL,
    OAUTH_SIGNIN_URL,
    OAUTH_2FA_VERIFY_URL,
    OAUTH_TOKEN_URL,
    OAUTH_V2_CLIENT_ID,
    OAUTH_REDIRECT_URI,
    OAUTH_USER_AGENT,
    OAUTH_TOKEN_USER_AGENT,
)

_LOGGER = logging.getLogger(__name__)

MIN_THROTTLE_TIME = 5
COMMAND_POLL_TIME = 1
MAX_RETRY = 120

# Camera action URL patterns
# Templates use {placeholder} for dynamic values filled at runtime
# Use "data" for static payloads, "data_template" for dynamic payloads
_CAMERA_ACTION_PATTERNS = {
    "mini": {
        "base_template": (
            "/api/v1/accounts/{account_id}/networks/{network}/owls/{camera_id}"
        ),
        "actions": {
            "arm": {
                "path": "config",
                "data_template": lambda p: {"enabled": p.get("arm_action") == "enable"},
            },
            "record": {"path": "clip"},
            "snap": {"path": "thumbnail"},
            "liveview": {
                "full_path": True,
                "path": (
                    "/api/v2/accounts/{account_id}"
                    "/networks/{network}/owls/{camera_id}/liveview"
                ),
                "data": {  # static payload
                    "intent": "liveview",
                    "motion_event_start_time": None,
                },
            },
        },
    },
    "doorbell": {
        "base_template": (
            "/api/v1/accounts/{account_id}/networks/{network}/doorbells/{camera_id}"
        ),
        "actions": {
            "arm": {
                "path_template": "{arm_action}",  # "enable" or "disable"
            },
            "record": {"path": "clip"},
            "snap": {"path": "thumbnail"},
            "liveview": {
                "full_path": True,
                "path": (
                    "/api/v2/accounts/{account_id}"
                    "/networks/{network}/doorbells/{camera_id}/liveview"
                ),
                "data": {  # static payload
                    "intent": "liveview",
                    "motion_event_start_time": None,
                },
            },
        },
    },
    "default": {
        "base_template": "/network/{network}/camera/{camera_id}",
        "actions": {
            "arm": {
                "path_template": "{arm_action}",  # "enable" or "disable"
            },
            "record": {"path": "clip"},
            "snap": {"path": "thumbnail"},
            "liveview": {
                "full_path": True,
                "path": (
                    "/api/v6/accounts/{account_id}"
                    "/networks/{network}/cameras/{camera_id}/liveview"
                ),
                "data": {  # static payload
                    "intent": "liveview",
                    "motion_event_start_time": None,
                },
            },
        },
    },
}


async def request_login(
    auth,
    url,
    login_data,
    is_refresh=False,
    is_retry=False,
):
    """
    OAuth login request.

    :param auth: Auth instance.
    :param url: Login url.
    :param login_data: Dictionary containing blink login data.
    :param is_retry:
    :param two_fa_code: 2FA code if required
    """

    headers = {
        "Content-Type": "application/x-www-form-urlencoded",
        "User-Agent": DEFAULT_USER_AGENT,
        "hardware_id": login_data.get("device_id", "Blinkpy"),
    }

    # Add 2FA code to headers if provided
    if "2fa_code" in login_data:
        headers["2fa-code"] = login_data["2fa_code"]

    # Prepare form data for OAuth
    form_data = {
        "username": login_data["username"],
        "client_id": OAUTH_CLIENT_ID,
        "scope": OAUTH_SCOPE,
    }

    if is_refresh:
        form_data["grant_type"] = OAUTH_GRANT_TYPE_REFRESH_TOKEN
        form_data["refresh_token"] = auth.refresh_token
    else:
        form_data["grant_type"] = OAUTH_GRANT_TYPE_PASSWORD
        form_data["password"] = login_data["password"]

    data = urlencode(form_data)

    return await auth.query(
        url=url,
        headers=headers,
        data=data,
        json_resp=False,
        reqtype="post",
        is_retry=is_retry,
        skip_refresh_check=True,
    )


async def request_tier(auth, url):
    """Get account tier information from blink servers."""
    headers = {
        "Content-Type": "application/x-www-form-urlencoded",
        "User-Agent": DEFAULT_USER_AGENT,
        "Authorization": f"Bearer {auth.token}",
    }

    return await auth.query(
        url=url,
        headers=headers,
        json_resp=True,
        reqtype="get",
    )


async def request_logout(blink):
    """Logout of blink servers."""
    url = (
        f"{blink.urls.base_url}/api/v4/account/{blink.account_id}"
        f"/client/{blink.client_id}/logout"
    )
    return await http_post(blink, url=url)


async def request_networks(blink):
    """Request all networks information."""
    url = f"{blink.urls.base_url}/networks"
    return await http_get(blink, url)


async def request_network_update(blink, network):
    """
    Request network update.

    :param blink: Blink instance.
    :param network: Sync module network id.
    """
    url = f"{blink.urls.base_url}/network/{network}/update"
    response = await http_post(blink, url)
    await wait_for_command(blink, response)
    return response


async def request_user(blink):
    """Get user information from blink servers."""
    url = f"{blink.urls.base_url}/user"
    return await http_get(blink, url)


async def request_network_status(blink, network):
    """
    Request network information.

    :param blink: Blink instance.
    :param network: Sync module network id.
    """
    url = f"{blink.urls.base_url}/network/{network}"
    return await http_get(blink, url)


async def request_syncmodule(blink, network):
    """
    Request sync module info.

    :param blink: Blink instance.
    :param network: Sync module network id.
    """
    url = f"{blink.urls.base_url}/network/{network}/syncmodules"
    return await http_get(blink, url)


@Throttle(seconds=MIN_THROTTLE_TIME)
async def request_system_arm(blink, network, **kwargs):
    """
    Arm system.

    :param blink: Blink instance.
    :param network: Sync module network id.
    """
    url = (
        f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}"
        f"/networks/{network}/state/arm"
    )
    response = await http_post(blink, url)
    await wait_for_command(blink, response)
    return response


@Throttle(seconds=MIN_THROTTLE_TIME)
async def request_system_disarm(blink, network, **kwargs):
    """
    Disarm system.

    :param blink: Blink instance.
    :param network: Sync module network id.
    """
    url = (
        f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}"
        f"/networks/{network}/state/disarm"
    )
    response = await http_post(blink, url)
    await wait_for_command(blink, response)
    return response


async def request_notification_flags(blink, **kwargs):
    """
    Get system notification flags.

    :param blink: Blink instance.
    """
    url = (
        f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}"
        "/notifications/configuration"
    )
    response = await http_get(blink, url)
    await wait_for_command(blink, response)
    return response


async def request_set_notification_flag(blink, data_dict):
    """
    Set a system notification flag.

    :param blink: Blink instance.
    :param data_dict: Dictionary of notifications to set.
    """
    url = (
        f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}"
        "/notifications/configuration"
    )
    data = dumps({"notifications": data_dict})
    response = await http_post(blink, url, data=data, json=False)
    await wait_for_command(blink, response)
    return response


async def request_command_status(blink, network, command_id):
    """
    Request command status.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param command_id: Command id to check.
    """
    url = f"{blink.urls.base_url}/network/{network}/command/{command_id}"
    return await http_get(blink, url)


async def request_command_done(blink, network, command_id):
    """
    Request command to be done.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param command_id: Command id to mark as done.
    """
    url = f"{blink.urls.base_url}/network/{network}/command/{command_id}/done/"
    return await http_post(blink, url)


@Throttle(seconds=MIN_THROTTLE_TIME)
async def request_homescreen(blink, **kwargs):
    """Request homescreen info."""
    url = f"{blink.urls.base_url}/api/v3/accounts/{blink.account_id}/homescreen"
    return await http_get(blink, url, json=False)


@Throttle(seconds=MIN_THROTTLE_TIME)
async def request_sync_events(blink, network, **kwargs):
    """
    Request events from sync module.

    :param blink: Blink instance.
    :param network: Sync module network id.
    """
    url = f"{blink.urls.base_url}/events/network/{network}"
    return await http_get(blink, url)


@Throttle(seconds=MIN_THROTTLE_TIME)
async def request_new_image(blink, network, camera_id, camera_type="", **kwargs):
    """
    Request to capture new thumbnail for camera.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param camera_id: Camera ID of camera to request new image from.
    :param camera_type: Camera type ("default", "mini", "doorbell").
    """
    return await request_camera_action(
        blink, network, camera_id, action="snap", camera_type=camera_type
    )


@Throttle(seconds=MIN_THROTTLE_TIME)
async def request_new_video(blink, network, camera_id, camera_type="", **kwargs):
    """
    Request to capture new video clip.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param camera_id: Camera ID of camera to request new video from.
    :param camera_type: Camera type ("default", "mini", "doorbell").
    """
    return await request_camera_action(
        blink, network, camera_id, action="record", camera_type=camera_type
    )


@Throttle(seconds=MIN_THROTTLE_TIME)
async def request_video_count(blink, **kwargs):
    """Request total video count."""
    url = f"{blink.urls.base_url}/api/v2/videos/count"
    return await http_get(blink, url)


async def request_videos(blink, time=None, page=0):
    """
    Perform a request for videos.

    :param blink: Blink instance.
    :param time: Get videos since this time.  In epoch seconds.
    :param page: Page number to get videos from.
    """
    timestamp = get_time(time)
    url = (
        f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}"
        f"/media/changed?since={timestamp}&page={page}"
    )
    return await http_get(blink, url)


async def request_cameras(blink, network):
    """
    Request all camera information.

    :param Blink: Blink instance.
    :param network: Sync module network id.
    """
    url = f"{blink.urls.base_url}/network/{network}/cameras"
    return await http_get(blink, url)


async def request_camera_info(blink, network, camera_id):
    """
    Request camera info for one camera.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param camera_id: Camera ID of camera to request info from.
    """
    url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/config"
    return await http_get(blink, url)


async def request_camera_usage(blink):
    """
    Request camera status.

    :param blink: Blink instance.
    """
    url = f"{blink.urls.base_url}/api/v1/camera/usage"
    return await http_get(blink, url)


async def request_camera_liveview(blink, network, camera_id, camera_type="", **kwargs):
    """
    Request camera liveview.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param camera_id: Camera ID of camera to request liveview from.
    :param camera_type: Camera type ("default", "mini", "doorbell").
    """
    return await request_camera_action(
        blink, network, camera_id, action="liveview", camera_type=camera_type
    )


async def request_camera_sensors(blink, network, camera_id):
    """
    Request camera sensor info for one camera.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param camera_id: Camera ID of camera to request sensor info from.
    """
    url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/signals"
    return await http_get(blink, url)


@Throttle(seconds=MIN_THROTTLE_TIME)
async def request_motion_detection_enable(
    blink, network, camera_id, camera_type="", **kwargs
):
    """
    Enable motion detection for a camera.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param camera_id: Camera ID of camera to enable.
    :param camera_type: Camera type ("default", "mini", "doorbell").
    """
    return await request_camera_action(
        blink,
        network,
        camera_id,
        action="arm",
        camera_type=camera_type,
        arm_action="enable",
    )


@Throttle(seconds=MIN_THROTTLE_TIME)
async def request_motion_detection_disable(
    blink, network, camera_id, camera_type="", **kwargs
):
    """
    Disable motion detection for a camera.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param camera_id: Camera ID of camera to disable.
    :param camera_type: Camera type ("default", "mini", "doorbell").
    """
    return await request_camera_action(
        blink,
        network,
        camera_id,
        action="arm",
        camera_type=camera_type,
        arm_action="disable",
    )


async def request_local_storage_manifest(blink, network, sync_id):
    """
    Update local manifest.

    Request creation of an updated manifest of video clips stored in
    sync module local storage.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param sync_id: ID of sync module.
    """
    url = (
        f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}"
        f"/networks/{network}/sync_modules/{sync_id}"
        f"/local_storage/manifest/request"
    )
    response = await http_post(blink, url)
    await wait_for_command(blink, response)
    return response


async def get_local_storage_manifest(blink, network, sync_id, manifest_request_id):
    """
    Request manifest of video clips stored in sync module local storage.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param sync_id: ID of sync module.
    :param manifest_request_id: Request ID of local storage manifest \
                                (requested creation of new manifest).
    """
    url = (
        f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}"
        f"/networks/{network}/sync_modules/{sync_id}"
        f"/local_storage/manifest/request/{manifest_request_id}"
    )
    return await http_get(blink, url)


async def request_local_storage_clip(blink, network, sync_id, manifest_id, clip_id):
    """
    Prepare video clip stored in the sync module to be downloaded.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param sync_id: ID of sync module.
    :param manifest_id: ID of local storage manifest (returned in manifest response).
    :param clip_id: ID of the clip.
    """
    url = blink.urls.base_url + string.Template(
        local_storage_clip_url_template()
    ).substitute(
        account_id=blink.account_id,
        network_id=network,
        sync_id=sync_id,
        manifest_id=manifest_id,
        clip_id=clip_id,
    )
    response = await http_post(blink, url)
    await wait_for_command(blink, response)
    return response


async def request_get_config(blink, network, camera_id, product_type="owl"):
    """
    Get camera configuration.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param camera_id: ID of camera
    :param product_type: Camera product type "owl" or "catalina"
    """
    if product_type == "owl":
        url = (
            f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}"
            f"/networks/{network}/owls/{camera_id}/config"
        )
    elif product_type == "catalina":
        url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/config"
    else:
        _LOGGER.info(
            "Camera %s with product type %s config get not implemented.",
            camera_id,
            product_type,
        )
        return None
    return await http_get(blink, url)


async def request_update_config(
    blink, network, camera_id, product_type="owl", data=None
):
    """
    Update camera configuration.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param camera_id: ID of camera
    :param product_type: Camera product type "owl" or "catalina"
    :param data: string w/JSON dict of parameters/values to update
    """
    if product_type == "owl":
        url = (
            f"{blink.urls.base_url}/api/v1/accounts/"
            f"{blink.account_id}/networks/{network}/owls/{camera_id}/config"
        )
    elif product_type == "catalina":
        url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/update"
    else:
        _LOGGER.info(
            "Camera %s with product type %s config update not implemented.",
            camera_id,
            product_type,
        )
        return None
    return await http_post(blink, url, json=False, data=data)


async def http_get(
    blink, url, stream=False, json=True, is_retry=False, timeout=TIMEOUT
):
    """
    Perform an http get request.

    :param url: URL to perform get request.
    :param stream: Stream response? True/FALSE
    :param json: Return json response? TRUE/False
    :param is_retry: Is this part of a re-auth attempt?
    """
    _LOGGER.debug("Making GET request to %s", url)
    return await blink.auth.query(
        url=url,
        headers=blink.auth.header,
        reqtype="get",
        stream=stream,
        json_resp=json,
        is_retry=is_retry,
    )


async def http_post(blink, url, is_retry=False, data=None, json=True, timeout=TIMEOUT):
    """
    Perform an http post request.

    :param url: URL to perform post request.
    :param is_retry: Is this part of a re-auth attempt?
    :param data: str body for post request
    :param json: Return json response? TRUE/False
    """
    _LOGGER.debug("Making POST request to %s", url)
    return await blink.auth.query(
        url=url,
        headers=blink.auth.header,
        reqtype="post",
        is_retry=is_retry,
        json_resp=json,
        data=data,
    )


async def request_camera_action(
    blink, network, camera_id, action, camera_type="", **kwargs
):
    """
    Perform camera actions for different camera types.

    :param blink: Blink instance.
    :param network: Sync module network id.
    :param camera_id: Camera ID.
    :param action: Action type ("arm", "record", "snap", "liveview").
    :param camera_type: Camera type ("default", "mini", "doorbell").
    :param **kwargs: Additional parameters for substitution (e.g., arm_action).
    """
    camera_type = camera_type or "default"
    if camera_type not in _CAMERA_ACTION_PATTERNS:
        raise ValueError(f"Unsupported camera type: {camera_type}")

    pattern = _CAMERA_ACTION_PATTERNS[camera_type]
    if action not in pattern["actions"]:
        raise ValueError(
            f"Unsupported action '{action}' for camera type '{camera_type}'"
        )

    # Get action and build base URL
    action_config = pattern["actions"][action]
    base_url = pattern["base_template"].format(
        account_id=blink.account_id,
        network=network,
        camera_id=camera_id,
    )

    # Build action path
    if "path_template" in action_config:
        # Dynamic path using template substitution
        path = action_config["path_template"].format(**kwargs)
    else:
        # Static path
        path = action_config.get("path", "")

    # Build full URL
    if action_config.get("full_path"):
        # For liveview on default cameras, path contains full path template
        url = blink.urls.base_url + path.format(
            account_id=blink.account_id,
            network=network,
            camera_id=camera_id,
        )
    else:
        # Standard URL construction
        url = f"{blink.urls.base_url}{base_url}/{path}"

    # Prepare request data
    data = None
    if "data_template" in action_config:
        # Dynamic payload with runtime value substitution
        data = dumps(action_config["data_template"](kwargs))
    elif "data" in action_config:
        # Static payload
        data = dumps(action_config["data"])

    # Execute request
    response = await http_post(blink, url, data=data)
    await wait_for_command(blink, response)
    return response


async def wait_for_command(blink, json_data: dict) -> bool:
    """Wait for command to complete."""
    _LOGGER.debug("Command Wait %s", json_data)
    try:
        network_id = json_data.get("network_id")
        command_id = json_data.get("id")
    except AttributeError:
        _LOGGER.exception("No network_id or id in response")
        return False
    if command_id and network_id:
        for _ in range(0, MAX_RETRY):
            _LOGGER.debug("Making GET request waiting for command")
            status = await request_command_status(blink, network_id, command_id)
            _LOGGER.debug("command status %s", status)
            if status:
                if status.get("status_code", 0) != 908:
                    return False
                if status.get("complete"):
                    return True
            await sleep(COMMAND_POLL_TIME)
        return False  # Timeout after MAX_RETRY attempts
    else:
        _LOGGER.debug("No network_id or id in response")
        return False


# OAuth v2 Authorization Code Flow + PKCE functions


async def oauth_authorize_request(auth, hardware_id, code_challenge):
    """
    Step 1: Initial authorization request.

    Args:
        auth: Auth instance
        hardware_id: Device hardware ID (UUID)
        code_challenge: PKCE code challenge

    Returns:
        bool: True if successful

    """
    params = {
        "app_brand": "blink",
        "app_version": "50.1",
        "client_id": OAUTH_V2_CLIENT_ID,
        "code_challenge": code_challenge,
        "code_challenge_method": "S256",
        "device_brand": "Apple",
        "device_model": "iPhone16,1",
        "device_os_version": "26.1",
        "hardware_id": hardware_id,
        "redirect_uri": OAUTH_REDIRECT_URI,
        "response_type": "code",
        "scope": OAUTH_SCOPE,
    }

    headers = {
        "User-Agent": OAUTH_USER_AGENT,
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
    }

    response = await auth.session.get(
        OAUTH_AUTHORIZE_URL, params=params, headers=headers
    )

    return response.status == 200


async def oauth_get_signin_page(auth):
    """
    Step 2: Get signin page and extract CSRF token.

    Args:
        auth: Auth instance

    Returns:
        str: CSRF token or None

    """
    headers = {
        "User-Agent": OAUTH_USER_AGENT,
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
    }

    response = await auth.session.get(OAUTH_SIGNIN_URL, headers=headers)

    if response.status != 200:
        return None

    html = await response.text()

    # Extract CSRF token from oauth-args script tag
    try:
        parser = OAuthArgsParser()
        parser.feed(html)
        if parser.csrf_token:
            return parser.csrf_token
    except Exception as error:
        _LOGGER.error("Failed to extract CSRF token: %s", error)

    return None


async def oauth_signin(auth, email, password, csrf_token):
    """
    Step 3: Submit login credentials.

    Args:
        auth: Auth instance
        email: User email
        password: User password
        csrf_token: CSRF token from signin page

    Returns:
        str: "SUCCESS", "2FA_REQUIRED", or None on failure

    """
    headers = {
        "User-Agent": OAUTH_USER_AGENT,
        "Accept": "*/*",
        "Content-Type": "application/x-www-form-urlencoded",
        "Origin": "https://api.oauth.blink.com",
        "Referer": OAUTH_SIGNIN_URL,
    }

    data = {
        "username": email,
        "password": password,
        "csrf-token": csrf_token,
    }

    response = await auth.session.post(
        OAUTH_SIGNIN_URL, headers=headers, data=data, allow_redirects=False
    )

    if response.status == 412:
        # 2FA required
        return "2FA_REQUIRED"

    if response.status == 202:
        response_text = await response.text()
        try:
            response_json = json.loads(response_text)
        except json.JSONDecodeError:
            response_json = {}

        if (
            response_json.get("tsv_state")
            or response_json.get("tsv_methods")
            or response_json.get("next_time_in_secs")
        ):
            # 2FA required (new response format with 202 status code)
            return "2FA_REQUIRED"

    elif response.status in [301, 302, 303, 307, 308]:
        # Success without 2FA
        return "SUCCESS"

    _LOGGER.error(
        "OAuth signin failed: status=%s body=%s",
        response.status,
        response_text[:800],
    )

    return None


async def oauth_verify_2fa(auth, csrf_token, twofa_code):
    """
    Step 3b: Verify 2FA code.

    Args:
        auth: Auth instance
        csrf_token: CSRF token
        twofa_code: 2FA code from user

    Returns:
        bool: True if verification successful

    """
    headers = {
        "User-Agent": OAUTH_USER_AGENT,
        "Accept": "*/*",
        "Content-Type": "application/x-www-form-urlencoded",
        "Origin": "https://api.oauth.blink.com",
        "Referer": OAUTH_SIGNIN_URL,
    }

    data = {
        "2fa_code": twofa_code,
        "csrf-token": csrf_token,
        "remember_me": "false",
    }

    response = await auth.session.post(OAUTH_2FA_VERIFY_URL, headers=headers, data=data)

    if response.status == 201:
        try:
            result = await response.json()
            return result.get("status") == "auth-completed"
        except Exception as error:
            _LOGGER.error("Failed to parse 2FA response: %s", error)

    return False


async def oauth_get_authorization_code(auth):
    """
    Step 4: Get authorization code from authorize endpoint.

    Args:
        auth: Auth instance

    Returns:
        str: Authorization code or None

    """
    headers = {
        "User-Agent": OAUTH_USER_AGENT,
        "Accept": "*/*",
        "Referer": OAUTH_SIGNIN_URL,
    }

    response = await auth.session.get(
        OAUTH_AUTHORIZE_URL, headers=headers, allow_redirects=False
    )

    if response.status in [301, 302, 303, 307, 308]:
        location = response.headers.get("Location", "")

        # Extract code from URL: https://blink.com/.../end?code=XXX&state=YYY
        parsed = urlparse(location)
        params = parse_qs(parsed.query)

        if "code" in params:
            return params["code"][0]

    return None


async def oauth_exchange_code_for_token(auth, code, code_verifier, hardware_id):
    """
    Step 5: Exchange authorization code for access token.

    Args:
        auth: Auth instance
        code: Authorization code
        code_verifier: PKCE code verifier
        hardware_id: Device hardware ID

    Returns:
        dict: Token data or None

    """
    headers = {
        "User-Agent": OAUTH_TOKEN_USER_AGENT,
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "*/*",
    }

    data = {
        "app_brand": "blink",
        "client_id": OAUTH_V2_CLIENT_ID,
        "code": code,
        "code_verifier": code_verifier,
        "grant_type": "authorization_code",
        "hardware_id": hardware_id,
        "redirect_uri": OAUTH_REDIRECT_URI,
        "scope": OAUTH_SCOPE,
    }

    response = await auth.session.post(
        OAUTH_TOKEN_URL, headers=headers, data=urlencode(data)
    )

    if response.status == 200:
        return await response.json()

    return None


async def oauth_refresh_token(auth, refresh_token, hardware_id):
    """
    Refresh access token using refresh_token.

    Args:
        auth: Auth instance
        refresh_token: Refresh token
        hardware_id: Device hardware ID

    Returns:
        dict: Token data or None

    """
    headers = {
        "User-Agent": OAUTH_TOKEN_USER_AGENT,
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "*/*",
    }

    data = {
        "grant_type": "refresh_token",
        "refresh_token": refresh_token,
        "client_id": OAUTH_V2_CLIENT_ID,
        "scope": OAUTH_SCOPE,
        "hardware_id": hardware_id,
    }

    response = await auth.session.post(
        OAUTH_TOKEN_URL, headers=headers, data=urlencode(data)
    )

    if response.status == 200:
        return await response.json()

    return None
