"""
Exchange page tasks — open via nav hover, then read Suggested Teams + Active Exchanges.
"""
import asyncio
import re
import time

from playwright.async_api import Page, Locator

from app.automation.helpers import human_delay
from app.automation.selectors import (
    ACTIVE_EXCHANGES_ITEM,
    SUGGESTED_TEAMS_LIST,
    SUGGESTED_TEAMS_UL,
    SEARCH_INPUT,
)
from app.automation.session_store import session_store
from app.automation.tasks.login import ensure_hudl_session
from app.automation.tasks.popup import close_hudl_popups
from app.automation.tasks.permissions import raise_if_hudl_forbidden
from app.automation.team_parser import parse_team_li


async def open_exchange(page: Page, team_id: str | None = None) -> dict:
    """
    Open Exchanges for a specific Hudl team_id (from Playwright team switch).

    Requires dynamic team_id — opens /manage/exchanges/{team_id}.
    """
    login_result = await ensure_hudl_session(page)
    await close_hudl_popups(page)

    explicit_id = (team_id or "").strip()
    if not explicit_id.isdigit():
        raise ValueError(
            "team_id required (dynamic from Playwright team switch). "
            "Select Football/Basketball in Organizations first."
        )

    target = f"https://www.hudl.com/manage/exchanges/{explicit_id}"
    print(f"Direct navigating to exchanges at {target} (team_id={explicit_id})")
    session_store.set_exchanges_url(target)
    await page.goto(target, wait_until="domcontentloaded")
    await human_delay(1.5, 2.5)
    await close_hudl_popups(page)

    # Clear any leftover exchange wizard so suggested teams are visible.
    try:
        from app.automation.tasks.cancel_back import _close_smooth_modal_wizard, _modal_open

        if await _modal_open(page):
            await _close_smooth_modal_wizard(page)
    except Exception:
        pass

    # Mirror Hudl 403: "You need permission to access this page."
    await raise_if_hudl_forbidden(page, team_id=explicit_id, area="exchange")

    session_store.mark_logged_in(page.url)
    result = {
        "status": "ok",
        "action": "open_exchange",
        "url": page.url,
        "team_id": explicit_id,
        "message": f"Opened Exchange for team {explicit_id}",
    }
    storage_warning = login_result.get("storage_warning") or session_store.get_storage_warning()
    if storage_warning:
        result["storage_warning"] = storage_warning
    return result


async def _scroll_until_stable(page: Page, items: Locator, max_rounds: int = 12) -> int:
    """Scroll page so lazy-loaded list items appear."""
    prev_count = 0
    stable_rounds = 0

    for _ in range(max_rounds):
        count = await items.count()
        if count == prev_count:
            stable_rounds += 1
            if stable_rounds >= 2:
                break
        else:
            stable_rounds = 0
        prev_count = count
        await page.mouse.wheel(0, 700)
        await human_delay(0.6, 1)

    return await items.count()


async def _scroll_active_exchanges_list(page: Page, list_locator: Locator, max_rounds: int = 20) -> int:
    """Scroll the Hudl sidebar list container so every exchange row is in the DOM."""
    container = page.locator("#ex-list-current").first
    prev_count = 0
    stable_rounds = 0

    for _ in range(max_rounds):
        count = await list_locator.count()
        if count == prev_count:
            stable_rounds += 1
            if stable_rounds >= 2:
                break
        else:
            stable_rounds = 0
        prev_count = count

        if await container.count() > 0:
            await container.evaluate("el => { el.scrollTop = el.scrollHeight; }")
        else:
            await page.mouse.wheel(0, 700)
        await human_delay(0.5, 0.8)

    return await list_locator.count()


async def _scroll_exchange_row_into_view(page: Page, li: Locator) -> None:
    """Scroll the sidebar container so the target exchange row is visible before click."""
    container = page.locator("#ex-list-current").first
    if await container.count() > 0:
        await li.evaluate(
            """(el) => {
                const container = document.getElementById('ex-list-current');
                if (!container) {
                    el.scrollIntoView({ block: 'center' });
                    return;
                }
                const elRect = el.getBoundingClientRect();
                const containerRect = container.getBoundingClientRect();
                if (elRect.top < containerRect.top) {
                    container.scrollTop -= (containerRect.top - elRect.top + 24);
                } else if (elRect.bottom > containerRect.bottom) {
                    container.scrollTop += (elRect.bottom - containerRect.bottom + 24);
                }
            }"""
        )
    else:
        await li.scroll_into_view_if_needed()
    await asyncio.sleep(0.2)


async def _get_exchange_href(li: Locator, page: Page | None = None) -> str | None:
    href = await li.evaluate(
        """(el) => {
            const link = el.querySelector('a[href*="/exchanges/"]');
            if (link) return link.getAttribute('href');
            const parent = el.closest('a[href*="/exchanges/"]');
            if (parent) return parent.getAttribute('href');
            return null;
        }"""
    )
    if href and "/exchanges/" in href:
        return href

    if page is not None:
        team_match = re.search(r"/teams/(\d+)", page.url)
        row_id = await li.get_attribute("data-exchange-id")
        if row_id and team_match:
            return f"/teams/{team_match.group(1)}/exchanges/{row_id}"
    return None


def _abs_hudl_url(href: str) -> str:
    if href.startswith("http"):
        return href
    if href.startswith("/"):
        return f"https://www.hudl.com{href}"
    return f"https://www.hudl.com/{href}"


def _exchange_id_from_url(url: str) -> str:
    if "/exchanges/" not in url:
        return ""
    return url.split("/exchanges/")[-1].split("?")[0].split("#")[0].strip("/")


async def _parse_active_exchange_li(li: Locator, index: int) -> dict | None:
    row = await li.evaluate(
        """(el) => {
            const who = el.querySelector('.ex-who');
            if (!who) return null;
            const divs = who.querySelectorAll(':scope > div');
            const date = (divs[0]?.innerText || '').trim();
            const name = (divs[1]?.innerText || '').trim();
            const message = (divs[2]?.innerText || '').trim();
            if (!name && !message) return null;
            const link = el.querySelector('a[href*="/exchanges/"]')
                || el.closest('a[href*="/exchanges/"]');
            const href = link ? link.getAttribute('href') : null;
            return { date, name, message, href };
        }"""
    )
    if not row:
        return None

    href = row.get("href")
    exchange_id = _exchange_id_from_url(href or "")

    return {
        "id": exchange_id or str(index),
        "time": row.get("date") or "",
        "name": row.get("name") or "",
        "process": row.get("message") or "",
        "href": href,
    }


async def _click_active_exchange_row(page: Page, li: Locator) -> None:
    """Click the exchange row using the same target Hudl uses in the sidebar."""
    await _scroll_exchange_row_into_view(page, li)

    link = li.locator('a[href*="/exchanges/"]').first
    if await link.count() > 0:
        try:
            await link.click(timeout=5000)
        except Exception:
            await link.click(force=True)
        await asyncio.sleep(0.6)
        return

    clickable = li.locator('xpath=.//div[contains(@class,"ex-who")]').first
    if await clickable.count() > 0:
        try:
            await clickable.click(timeout=5000)
        except Exception:
            await clickable.click(force=True)
    else:
        await li.scroll_into_view_if_needed()
        await li.evaluate("el => el.scrollIntoView({ block: 'center' })")
        try:
            await li.click(timeout=5000)
        except Exception:
            await li.click(force=True)

    await asyncio.sleep(0.6)


async def _open_active_exchange_row(
    page: Page,
    li: Locator,
    expected_name: str,
) -> str | None:
    """
    Open an exchange's detail view. Prefer direct URL navigation (most reliable),
    then fall back to clicking the sidebar row.
    """
    await _scroll_exchange_row_into_view(page, li)
    href = await _get_exchange_href(li, page)

    if href:
        target_url = _abs_hudl_url(href)
        await page.goto(target_url, wait_until="domcontentloaded", timeout=25000)
        await close_hudl_popups(page)
        await asyncio.sleep(1.0)
        return target_url

    await _click_active_exchange_row(page, li)
    return href


def _school_name_matches(current: str, expected: str) -> bool:
    current = current.strip().lower()
    expected = expected.strip().lower()
    if not current or not expected:
        return False
    return current == expected


async def _get_details_school_name(details_pane: Locator) -> str:
    school_locator = details_pane.locator(".ex-details-school").first
    if await school_locator.count() == 0:
        return ""
    return (await school_locator.inner_text()).strip()


async def _wait_for_details_school_name(
    page: Page,
    details_pane: Locator,
    expected_name: str,
    expected_href: str | None = None,
    timeout_s: float = 4.0,
) -> bool:
    """Wait until the details header reflects the selected exchange."""
    school_locator = details_pane.locator(".ex-details-school").first
    deadline = time.monotonic() + timeout_s

    while time.monotonic() < deadline:
        if await school_locator.count() > 0:
            current = (await school_locator.inner_text()).strip()
            if _school_name_matches(current, expected_name):
                return True
        await asyncio.sleep(0.15)
    return False


async def _navigate_to_exchanges_home(page: Page) -> None:
    """Open the exchanges list page so the sidebar shows every active row reliably."""
    if "exchange" not in page.url.lower():
        await open_exchange(page)
        await asyncio.sleep(0.5)
        return

    await close_hudl_popups(page)
    path = page.url.split("?")[0].rstrip("/")
    if path.endswith("/exchanges") or re.search(r"/manage/exchanges/\d+$", path):
        return

    match = re.search(r"(https?://[^/]+/teams/\d+)/exchanges", page.url)
    if match:
        list_url = f"{match.group(1)}/exchanges"
    else:
        manage_match = re.search(r"(https?://[^/]+/manage/exchanges/\d+)", page.url)
        if manage_match:
            list_url = manage_match.group(1)
        else:
            await open_exchange(page)
            await asyncio.sleep(0.5)
            return

    await page.goto(list_url, wait_until="domcontentloaded", timeout=25000)
    await close_hudl_popups(page)
    await asyncio.sleep(0.8)


async def _ensure_exchange_list_visible(page: Page, full_scroll: bool = False) -> Locator:
    """Ensure the exchange sidebar list is on screen. Skip full scroll unless loading all rows."""
    await _navigate_to_exchanges_home(page)

    list_locator = page.locator("#ex-list-current ul li")
    await list_locator.first.wait_for(state="visible", timeout=10000)
    if full_scroll:
        await _scroll_active_exchanges_list(page, list_locator)
    return list_locator


async def _resolve_active_exchange_target(
    page: Page,
    exchange_name: str,
    exchange_index: int | None = None,
    exchange_id: str | None = None,
) -> tuple[Locator, dict, int] | None:
    """
    Resolve the target exchange row. Priority:
    1) School name (most reliable — UI index can differ from live Hudl list)
    2) Hudl exchange id from sidebar link
    3) Exact list index only when it matches the requested name
    """
    list_locator = await _ensure_exchange_list_visible(page, full_scroll=True)
    count = await list_locator.count()
    parsed_rows: list[tuple[int, dict]] = []

    for i in range(count):
        row = await _parse_active_exchange_li(list_locator.nth(i), i)
        if row:
            parsed_rows.append((i, row))

    if exchange_name:
        name_matches = [
            (i, row) for i, row in parsed_rows if _school_name_matches(row["name"], exchange_name)
        ]
        if name_matches:
            if exchange_index is not None:
                for i, row in name_matches:
                    if i == exchange_index:
                        return list_locator.nth(i), row, i
            i, row = name_matches[0]
            return list_locator.nth(i), row, i

    if exchange_id:
        for i, row in parsed_rows:
            if row.get("id") == exchange_id:
                return list_locator.nth(i), row, i

    if exchange_index is not None:
        for i, row in parsed_rows:
            if i == exchange_index:
                return list_locator.nth(i), row, i

    return None


async def _ensure_exchange_index_available(
    page: Page,
    list_locator: Locator,
    exchange_index: int,
    max_steps: int = 8,
) -> bool:
    """Scroll the sidebar only enough to make the target index available."""
    container = page.locator("#ex-list-current").first
    steps = 0
    while await list_locator.count() <= exchange_index and steps < max_steps:
        if await container.count() > 0:
            await container.evaluate("el => { el.scrollTop += el.clientHeight; }")
        else:
            await page.mouse.wheel(0, 500)
        await asyncio.sleep(0.2)
        steps += 1
    return exchange_index < await list_locator.count()


async def get_exchange_data(page: Page, team_id: str | None = None) -> dict:
    """
    GET API: Suggested Teams + Active Exchanges (scrolls if list is long).
    """
    explicit = (team_id or "").strip()
    current = page.url or ""
    needs_open = "exchange" not in current.lower()
    if explicit.isdigit() and f"/exchanges/{explicit}" not in current and f"/teams/{explicit}/exchanges" not in current:
        needs_open = True

    if needs_open:
        await open_exchange(page, team_id=explicit or None)
    else:
        await ensure_hudl_session(page)
        await close_hudl_popups(page)
        await human_delay(1, 1.5)
        await raise_if_hudl_forbidden(page, team_id=explicit or None, area="exchange")

    try:
        await page.locator(SEARCH_INPUT).first.wait_for(state="visible", timeout=8000)
    except Exception as e:
        await raise_if_hudl_forbidden(page, team_id=explicit or None, area="exchange")
        print(f"SEARCH_INPUT not visible on exchanges page: {e}. Reloading...")
        try:
            await page.reload(wait_until="domcontentloaded")
            await human_delay(3, 5)
            await close_hudl_popups(page)
        except Exception as reload_err:
            print(f"Reload failed: {reload_err}")
        await raise_if_hudl_forbidden(page, team_id=explicit or None, area="exchange")
        try:
            await page.locator(SEARCH_INPUT).first.wait_for(state="visible", timeout=8000)
        except Exception:
            await raise_if_hudl_forbidden(page, team_id=explicit or None, area="exchange")
            print("Still not visible. Attempting to open exchange from scratch...")
            await open_exchange(page, team_id=explicit or None)
            await page.locator(SEARCH_INPUT).first.wait_for(state="visible", timeout=15000)

    suggested_locator = page.locator(SUGGESTED_TEAMS_LIST)
    active_list_locator = page.locator("#ex-list-current ul li")

    try:
        await page.locator(SUGGESTED_TEAMS_UL).first.wait_for(state="attached", timeout=5000)
    except Exception:
        pass

    await _scroll_until_stable(page, suggested_locator)
    await _scroll_active_exchanges_list(page, active_list_locator)

    suggested_teams = []
    for i in range(await suggested_locator.count()):
        row = await parse_team_li(suggested_locator.nth(i), i)
        if row:
            suggested_teams.append(row)

    active_exchanges = []
    active_count = await active_list_locator.count()
    for i in range(active_count):
        row = await _parse_active_exchange_li(active_list_locator.nth(i), i)
        if row:
            active_exchanges.append(row)

    print(
        f"Exchange data scraped: {len(suggested_teams)} suggested, "
        f"{len(active_exchanges)} active (url={page.url})"
    )

    result = {
        "status": "ok",
        "url": page.url,
        "suggested_teams": suggested_teams,
        "suggested_teams_count": len(suggested_teams),
        "active_exchanges": active_exchanges,
        "active_exchanges_count": len(active_exchanges),
    }
    storage_warning = session_store.get_storage_warning()
    if storage_warning:
        result["storage_warning"] = storage_warning
    return result


async def get_active_exchanges(page: Page, team_id: str | None = None, full_scroll: bool = False) -> dict:
    """
    GET task to fetch currently active exchanges from //div[@id="ex-list-current"]/ul/li
    Inside each li, there is .//div[@class="ex-who"] which has 3 direct child divs:
    ./div[1]  <<-- date
    ./div[2]  <<-- name
    ./div[3]  <<-- message
    """
    try:
        explicit = (team_id or "").strip()
        current = page.url or ""
        needs_open = "exchange" not in current.lower()
        if explicit.isdigit() and f"/exchanges/{explicit}" not in current and f"/teams/{explicit}/exchanges" not in current:
            needs_open = True

        if needs_open:
            await open_exchange(page, team_id=explicit or None)
        else:
            await close_hudl_popups(page)
            await human_delay(0.5, 1)

        list_locator = await _ensure_exchange_list_visible(page, full_scroll=full_scroll or True)
        count = await list_locator.count()
        exchanges = []
        for i in range(count):
            row = await _parse_active_exchange_li(list_locator.nth(i), i)
            if row:
                exchanges.append({
                    "id": row["id"],
                    "date": row["time"],
                    "name": row["name"],
                    "message": row["process"],
                    "href": row.get("href"),
                })
        
        return {
            "status": "ok",
            "action": "get_active_exchanges",
            "exchanges": exchanges,
            "count": len(exchanges),
            "team_id": explicit or None,
            "url": page.url,
        }
    except Exception as e:
        return {"status": "error", "message": str(e)}


async def get_active_exchange_details(
    page: Page,
    exchange_name: str,
    exchange_index: int | None = None,
    exchange_id: str | None = None,
    href: str | None = None,
    team_id: str | None = None,
) -> dict:
    """
    POST task to click a specific active exchange by school name (preferred)
    and scrape all rich data inside `div.ex-details`.
    """
    try:
        if not exchange_name and exchange_index is None and not exchange_id and not href:
            return {"status": "error", "message": "Exchange name, index, id, or href is required."}

        explicit_team = (team_id or "").strip()
        if explicit_team.isdigit():
            current = page.url or ""
            if (
                f"/exchanges/{explicit_team}" not in current
                and f"/teams/{explicit_team}/exchanges" not in current
            ):
                await open_exchange(page, team_id=explicit_team)

        if exchange_index is not None and exchange_index < 0:
            return {"status": "error", "message": f"Exchange index {exchange_index} is invalid."}

        li = None
        expected_name = exchange_name
        expected_href = href
        navigated_directly = False
        details_pane = page.locator("div.ex-details").first

        # Check if we can do client-side SPA navigation instead of page.goto
        spa_navigated = False
        if "exchanges" in page.url.lower():
            print(f"Server Optimization: Already on exchanges page. Attempting client-side SPA click for '{exchange_name or exchange_id}'.")
            try:
                # Get the current list of items without force scrolling
                list_locator = page.locator("#ex-list-current ul li")
                await list_locator.first.wait_for(state="visible", timeout=3000)
                count = await list_locator.count()
                
                # Check for match in existing items
                target_li = None
                target_row = None
                target_idx = -1
                
                for i in range(count):
                    li_loc = list_locator.nth(i)
                    row = await _parse_active_exchange_li(li_loc, i)
                    if row:
                        matches = False
                        if exchange_id and row.get("id") == exchange_id:
                            matches = True
                        elif href and row.get("href") == href:
                            matches = True
                        elif exchange_name and _school_name_matches(row["name"], exchange_name):
                            matches = True
                            
                        if matches:
                            target_li = li_loc
                            target_row = row
                            target_idx = i
                            break
                
                if target_li:
                    print(f"Found match at index {target_idx}. Clicking row.")
                    await _click_active_exchange_row(page, target_li)
                    await details_pane.wait_for(state="visible", timeout=5000)
                    expected_name = target_row["name"]
                    expected_href = target_row.get("href") or href
                    
                    if await _wait_for_details_school_name(page, details_pane, expected_name, expected_href, timeout_s=4.0):
                        print("Client-side click succeeded. SPA content updated.")
                        spa_navigated = True
            except Exception as spa_err:
                print(f"Client-side SPA click failed or timed out: {spa_err}. Falling back to full page load.")

        if not spa_navigated:
            if href and "/exchanges/" in href:
                target_url = _abs_hudl_url(href)
                await page.goto(target_url, wait_until="domcontentloaded", timeout=25000)
                await close_hudl_popups(page)
                
                # Wait dynamically until the school name is loaded/non-empty
                school_locator = details_pane.locator(".ex-details-school").first
                await school_locator.wait_for(state="visible", timeout=5000)
                for _ in range(30):
                    scraped_name = (await school_locator.inner_text()).strip()
                    if scraped_name:
                        break
                    await asyncio.sleep(0.1)
                    
                await details_pane.wait_for(state="visible", timeout=10000)
                scraped_name = await _get_details_school_name(details_pane)
                if not scraped_name:
                    return {"status": "error", "message": f"Could not load exchange at '{href}'."}
                if exchange_name and not _school_name_matches(scraped_name, exchange_name):
                    return {
                        "status": "error",
                        "message": (
                            f"Exchange href opened '{scraped_name}' but expected '{exchange_name}'."
                        ),
                    }
                expected_name = scraped_name
                navigated_directly = True
            elif exchange_id:
                team_match = re.search(r"/teams/(\d+)", page.url)
                if not team_match:
                    await _navigate_to_exchanges_home(page)
                    team_match = re.search(r"/teams/(\d+)", page.url)
                if not team_match:
                    return {"status": "error", "message": "Could not resolve Hudl team for exchange navigation."}
                expected_href = f"/teams/{team_match.group(1)}/exchanges/{exchange_id}"
                target_url = _abs_hudl_url(expected_href)
                await page.goto(target_url, wait_until="domcontentloaded", timeout=25000)
                await close_hudl_popups(page)
                
                # Wait dynamically until the school name is loaded/non-empty
                school_locator = details_pane.locator(".ex-details-school").first
                await school_locator.wait_for(state="visible", timeout=5000)
                for _ in range(30):
                    scraped_name = (await school_locator.inner_text()).strip()
                    if scraped_name:
                        break
                    await asyncio.sleep(0.1)
                    
                await details_pane.wait_for(state="visible", timeout=10000)
                scraped_name = await _get_details_school_name(details_pane)
                if not scraped_name:
                    return {"status": "error", "message": f"Could not load exchange '{exchange_id}'."}
                if exchange_name and not _school_name_matches(scraped_name, exchange_name):
                    return {
                        "status": "error",
                        "message": (
                            f"Exchange id opened '{scraped_name}' but expected '{exchange_name}'."
                        ),
                    }
                expected_name = scraped_name
                navigated_directly = True
            else:
                resolved = await _resolve_active_exchange_target(
                    page, exchange_name, exchange_index, exchange_id
                )
                if not resolved:
                    label = exchange_name or str(exchange_index)
                    return {
                        "status": "error",
                        "message": f"Exchange '{label}' was not found in the active list.",
                    }
                li, row, exchange_index = resolved
                expected_name = row["name"]
                expected_href = row.get("href") or expected_href

                if expected_href and "/exchanges/" in expected_href:
                    target_url = _abs_hudl_url(expected_href)
                    await page.goto(target_url, wait_until="domcontentloaded", timeout=25000)
                    await close_hudl_popups(page)
                    
                    # Wait dynamically until the school name is loaded/non-empty
                    school_locator = details_pane.locator(".ex-details-school").first
                    await school_locator.wait_for(state="visible", timeout=5000)
                    for _ in range(30):
                        scraped_name = (await school_locator.inner_text()).strip()
                        if scraped_name:
                            break
                        await asyncio.sleep(0.1)
                        
                    await details_pane.wait_for(state="visible", timeout=10000)
                    scraped_name = await _get_details_school_name(details_pane)
                    if not scraped_name:
                        return {
                            "status": "error",
                            "message": f"Could not load exchange at '{expected_href}'.",
                        }
                    if exchange_name and not _school_name_matches(scraped_name, exchange_name):
                        return {
                            "status": "error",
                            "message": (
                                f"Exchange href opened '{scraped_name}' but expected '{exchange_name}'."
                            ),
                        }
                    expected_name = scraped_name
                    navigated_directly = True
                    li = None

            if not navigated_directly and li is not None:
                opened_href = await _open_active_exchange_row(page, li, expected_name)
                if not expected_href and opened_href:
                    expected_href = opened_href

                await details_pane.wait_for(state="visible", timeout=10000)
                if not await _wait_for_details_school_name(
                    page, details_pane, expected_name, expected_href, timeout_s=8.0
                ):
                    await _open_active_exchange_row(page, li, expected_name)
                    if not await _wait_for_details_school_name(
                        page, details_pane, expected_name, expected_href, timeout_s=6.0
                    ):
                        current_name = await _get_details_school_name(details_pane)
                        return {
                            "status": "error",
                            "message": (
                                f"Details panel did not update to '{expected_name}'"
                                + (f" (still showing '{current_name}')." if current_name else ".")
                            ),
                        }

        # Scrape Header details
        header = details_pane.locator('.ex-details-header').first
        header_class = await header.get_attribute("class") or ""
        school_name = (await header.locator('.ex-details-school').inner_text()).strip()
        if not _school_name_matches(school_name, expected_name):
            return {
                "status": "error",
                "message": (
                    f"Details panel shows '{school_name}' but expected '{expected_name}'."
                ),
            }

        # Scrape dynamic header buttons
        buttons = []
        # Support button and span with .uni-btn class
        btn_locators = header.locator('.ex-actions .uni-btn, .ex-actions button')
        for i in range(await btn_locators.count()):
            btn = btn_locators.nth(i)
            text = (await btn.inner_text()).strip()
            class_attr = await btn.get_attribute("class") or ""
            
            # Map button type
            action_type = "unknown"
            if "ex-action-archive" in class_attr:
                action_type = "delete"
            elif "ex-action-cancel" in class_attr:
                action_type = "cancel"
            elif "ex-action-add" in class_attr:
                action_type = "add"
            elif "ex-action-choose" in class_attr:
                action_type = "choose"
            elif "ex-action-decline" in class_attr:
                action_type = "decline"
            
            buttons.append({
                "text": text,
                "type": action_type,
                "classes": class_attr
            })

        # Scrape main sections (message body, time, playlists)
        time_text = ""
        time_locator = details_pane.locator('.ex-contact-time').first
        if await time_locator.count() > 0:
            time_text = (await time_locator.inner_text()).strip()

        sender_text = ""
        for sender_sel in (
            ".ex-details-contact .ex-contact-name h4.uni-subhead",
            ".ex-details-contact h4.uni-subhead",
            ".ex-details-contact h4",
            ".ex-contact-name h4",
        ):
            sender_locator = details_pane.locator(sender_sel).first
            if await sender_locator.count() > 0:
                sender_text = (await sender_locator.inner_text()).strip()
                if sender_text:
                    break

        message_text = ""
        for message_sel in (
            ".ex-details-message",
            ".ex-details-contact .ex-details-message",
            ".ex-contact-message",
            ".ex-details-body",
        ):
            message_locator = details_pane.locator(message_sel).first
            if await message_locator.count() > 0:
                message_text = (await message_locator.inner_text()).strip()
                if message_text:
                    break
        if message_text.startswith('"') and message_text.endswith('"'):
            message_text = message_text[1:-1]

        # Scrape Playlists
        playlists = []
        playlist_locators = details_pane.locator('.ex-details-playlist')
        for i in range(await playlist_locators.count()):
            pl = playlist_locators.nth(i)
            pl_name = ""
            name_loc = pl.locator('.ex-playlist-name')
            if await name_loc.count() > 0:
                pl_name = (await name_loc.inner_text()).strip()
                
            pl_length = ""
            len_loc = pl.locator('.ex-playlist-length')
            if await len_loc.count() > 0:
                pl_length = (await len_loc.inner_text()).strip()

            img_src = ""
            try:
                for img_sel in (
                    ".ex-playlist-thumb img",
                    ".ex-playlist-thumbnail img",
                    "img",
                ):
                    img_loc = pl.locator(img_sel)
                    if await img_loc.count() > 0:
                        img_src = await img_loc.first.get_attribute("src") or ""
                        if img_src:
                            break
            except Exception:
                img_src = ""

            playlists.append({
                "name": pl_name,
                "length": pl_length,
                "thumb_url": img_src
            })

        return {
            "status": "ok",
            "school_name": school_name,
            "header_class": header_class,
            "buttons": buttons,
            "time": time_text,
            "sender": sender_text,
            "message": message_text,
            "playlists": playlists
        }
    except Exception as e:
        return {"status": "error", "message": str(e)}


# Maps the semantic action type (from the frontend) to the Hudl CSS class
# that identifies the corresponding header button in `.ex-actions`.
_ACTION_TYPE_CLASS = {
    "delete": "ex-action-archive",
    "cancel": "ex-action-cancel",
    "decline": "ex-action-decline",
    "choose": "ex-action-choose",
    "add": "ex-action-add",
}


async def _confirm_hudl_action_modal(page: Page) -> bool:
    """
    After clicking a destructive action (archive/cancel/decline) Hudl usually
    shows a confirmation modal. Try to click its primary/confirm button.
    Returns True if a confirmation button was clicked.
    """
    confirm_selectors = (
        ".uni-modal .uni-btn-primary",
        ".uni-modal button.uni-btn-danger",
        ".uni-modal-actions button.uni-btn-primary",
        "[role='dialog'] button.uni-btn-primary",
    )
    for selector in confirm_selectors:
        btn = page.locator(selector).first
        try:
            if await btn.count() > 0 and await btn.is_visible():
                await btn.click()
                await human_delay(1.0, 1.8)
                return True
        except Exception:
            continue

    # Fallback: match a modal button by its text.
    confirm_texts = ("confirm", "yes", "archive", "delete", "cancel exchange", "remove")
    modal_btns = page.locator(".uni-modal button, [role='dialog'] button")
    try:
        for i in range(await modal_btns.count()):
            btn = modal_btns.nth(i)
            if not await btn.is_visible():
                continue
            text = (await btn.inner_text()).strip().lower()
            if any(t in text for t in confirm_texts):
                await btn.click()
                await human_delay(1.0, 1.8)
                return True
    except Exception:
        pass

    return False


async def click_active_exchange_button(
    page: Page,
    action_type: str,
    exchange_name: str,
    exchange_index: int | None = None,
    exchange_id: str | None = None,
    href: str | None = None,
) -> dict:
    """
    POST task: open a specific active exchange, then click its header action
    button (delete/cancel/decline/choose/add) and confirm any resulting modal.
    """
    action_type = (action_type or "").lower()
    target_class = _ACTION_TYPE_CLASS.get(action_type)
    if not target_class:
        return {
            "status": "error",
            "message": f"Unsupported action type '{action_type}'.",
        }

    # Reuse the existing navigation + verification logic to land on the
    # correct exchange detail pane.
    details = await get_active_exchange_details(
        page,
        exchange_name=exchange_name,
        exchange_index=exchange_index,
        exchange_id=exchange_id,
        href=href,
    )
    if not details or details.get("status") != "ok":
        return details or {
            "status": "error",
            "message": "Could not open the exchange to perform the action.",
        }

    try:
        details_pane = page.locator("div.ex-details").first
        header = details_pane.locator(".ex-details-header").first
        btn = header.locator(f".ex-actions .{target_class}").first

        if await btn.count() == 0:
            return {
                "status": "error",
                "message": (
                    f"No '{action_type}' button available for "
                    f"{details.get('school_name') or exchange_name}."
                ),
            }

        btn_text = (await btn.inner_text()).strip()
        await btn.scroll_into_view_if_needed()
        await btn.click()
        await human_delay(1.0, 1.8)

        confirmed = await _confirm_hudl_action_modal(page)
        await close_hudl_popups(page)
        await human_delay(0.8, 1.4)

        return {
            "status": "ok",
            "action": f"active_exchange_{action_type}",
            "confirmed": confirmed,
            "url": page.url,
            "message": (
                f"Clicked '{btn_text or action_type}' for "
                f"{details.get('school_name') or exchange_name}."
            ),
        }
    except Exception as e:
        return {"status": "error", "message": str(e)}


