"""
Account Service - Manages Microsoft account authentication and switching.
Handles user login and automatic account switching to the required Microsoft account.
"""

import os
import asyncio
import json
import re
from typing import Optional
from playwright.async_api import Page

from core.logger import get_logger


class AccountService:
    """
    Service responsible for account management decisions and operations.
    Handles user login and automatic account switching to the required Microsoft account.
    """

    def __init__(self, logger=None):
        """Initialize the account service with credentials from environment."""
        self.logger = logger or get_logger(__name__)
        self.accounts = json.loads(os.getenv("ACCOUNTS_ID", "{}"))
        self.password = os.getenv("ACCOUNTS_PASSWORD", "")

    async def _open_account_manager(self, page: Page) -> bool:
        """
        Opens the account manager dropdown using a stable XPath that does not rely on
        dynamic IDs, classes, or profile initials. Returns True if the dropdown was
        opened successfully.
        """
        # Candidate opener selectors (primary first). The primary relies on the
        # profile button being the last header button with an aria-expanded attr.
        # Fallbacks avoid using account-specific text or initials.
        opener_selectors = [
            "xpath=//header//button[@aria-expanded][last()]",
            "xpath=//button[contains(translate(@aria-label,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'account')][last()]",
            "xpath=//button[contains(., 'Sign out') or contains(., 'Sign in')][last()]",
            "css=#mectrl_main_trigger",
        ]

        # Post-click checks to verify the account menu/dialog opened.
        menu_indicators = [
            "xpath=(//div[@role='dialog' or @role='menu' or contains(@class,'dropdown')])[last()]",
            "text=Sign out",
            "text=Sign in with a different account",
            "css=#mectrl_main_body",
        ]

        for sel in opener_selectors:
            try:
                await page.wait_for_selector(sel, timeout=3000)
                # Hover before click (helps if element is overlayed)
                try:
                    await page.hover(sel)
                except Exception:
                    pass
                await page.click(sel)

                # After clicking, wait briefly and then check for any known menu indicator.
                for indicator in menu_indicators:
                    try:
                        await page.wait_for_selector(indicator, timeout=2500)
                        await asyncio.sleep(0.25)
                        return True
                    except Exception:
                        continue

                # If no indicator appeared, give a short pause and try the next opener.
                await asyncio.sleep(0.5)
            except Exception:
                continue

        return False

    async def _get_displayed_account_text(self, page: Page) -> str:
        """
        After the account manager dropdown is open, find and return the displayed
        account text (name or email). Uses a content-based XPath that searches for
        an element containing an email-like string (the '@' sign) as a reliable
        indicator of the signed-in account. Falls back to a few other strategies
        before returning an empty string.
        """
        # Look for the most-recently opened dialog/menu region and find the first
        # descendant DIV that contains an '@' (email). This is robust across
        # account types and does not use dynamic ids/classes.
        candidate_xpaths = [
            # Panel-like regions (dialog/menu) then descendant element with '@'
            "xpath=(//div[@role='dialog' or @role='menu' or contains(@class,'dropdown')])[last()]//div[contains(., '@')][1]",
            # Any descendant containing an '@' anywhere on the page (last opened)
            "xpath=(//div[contains(., '@')])[last()]",
            # Legacy fallback: well-known element id sometimes present
            "css=#mectrl_currentAccount_primary",
        ]

        for sel in candidate_xpaths:
            try:
                await page.wait_for_selector(sel, timeout=3000)
                text = (await page.inner_text(sel)).strip()
                if text:
                    return text
            except Exception:
                continue
        return ""

    async def is_child_account(self, page: Page, account_name: str = None) -> bool:
        """
        Checks if the currently logged-in account matches the requested account.
        If `account_name` is provided, verifies the displayed account matches that
        account's email exactly (preferred). If not provided, returns True if the
        displayed account matches any known account value.
        """
        try:
            await page.wait_for_load_state('networkidle', timeout=15000)
            try:
                await page.wait_for_selector('text=Family Safety', timeout=5000)
            except Exception:
                pass

            opened = await self._open_account_manager(page)
            if not opened:
                self.logger.error("Could not open account manager dropdown")
                return False

            current_account = await self._get_displayed_account_text(page)
            self.logger.info(f"Current account detected: {current_account}")

            # Try to extract an email from the displayed account text for precise matching
            email_match = re.search(r"[\w\.-]+@[\w\.-]+", current_account or "")
            displayed_email = email_match.group(0).lower() if email_match else (current_account or "").strip().lower()

            # If a specific account was requested, check that account only.
            if account_name:
                expected_email = self.accounts.get(account_name)
                if not expected_email:
                    self.logger.error(f"Requested account '{account_name}' not found in ACCOUNTS_ID environment.")
                    return False
                expected_email = expected_email.strip().lower()
                is_match = False
                if displayed_email and expected_email == displayed_email:
                    is_match = True
                else:
                    # fallback to substring check against visible text
                    is_match = expected_email in (current_account or "").lower()

                self.logger.info(f"Is current account equal to requested '{account_name}'? {is_match}")
                return is_match

            # Otherwise, return True if any configured account matches the displayed account
            for acc in self.accounts.values():
                if not acc:
                    continue
                acc_l = acc.strip().lower()
                if displayed_email and acc_l == displayed_email:
                    self.logger.info(f"Displayed account matches configured account {acc}")
                    return True
                if acc_l in (current_account or "").lower():
                    self.logger.info(f"Displayed account contains configured account value {acc}")
                    return True

            self.logger.info("No matching configured account found for displayed account")
            return False

        except Exception as e:
            self.logger.error(f"Failed to check current account: {e}")
            return False

    async def sign_in_with_different_account(self, page: Page, account_name: str = None) -> bool:
        """
        Signs in with the specified account using credentials from environment variables.
        """
        email = None
        self.logger.info(f"Preparing to sign in with account: {account_name or 'default'}")
        if account_name:
            email = self.accounts.get(account_name)
            self.logger.info(f"Retrieved email for account '{account_name or 'default'}': {email}")
        if not email or not self.password:
            self.logger.error("Account credentials not found in environment variables.")
            return False

        self.logger.info(f"Initiating sign-in with account: {account_name or 'default'} ({email})")
        await asyncio.sleep(2)

        # Try multiple selectors for the "Sign in with a different account" control
        signin_selectors = [
            "text=Sign in with a different account",
            "text=Use another account",
            "xpath=//button[contains(., 'Sign in with a different account') or contains(., 'Use another account')]",
            "text=Sign in with a different account, Use another account"
        ]
        clicked = False
        for sel in signin_selectors:
            try:
                await page.wait_for_selector(sel, timeout=3000)
                await page.click(sel)
                clicked = True
                break
            except Exception:
                continue

        # If not found, try opening account manager again and retry using stable opener
        if not clicked:
            opened = await self._open_account_manager(page)
            if opened:
                await asyncio.sleep(1)
                for sel in signin_selectors:
                    try:
                        await page.wait_for_selector(sel, timeout=3000)
                        await page.click(sel)
                        clicked = True
                        break
                    except Exception:
                        continue

        if not clicked:
            # Debug: capture screenshot and page content for investigation
            try:
                await page.screenshot(path="debug_signin_different_account.png")
            except Exception:
                pass
            try:
                content = await page.content()
                self.logger.debug(f"Page content on failure (truncated): {content[:2000]}")
            except Exception:
                pass
            self.logger.error("Could not find 'Sign in with a different account' control. Aborting sign-in.")
            return False

        await asyncio.sleep(2)

        self.logger.info("Filling in account credentials.")
        await page.wait_for_selector("input[type='email'], input[name='loginfmt']", timeout=15000)
        await page.fill("input[type='email'], input[name='loginfmt']", email)
        await page.click("button:has-text('Next'), input[type='submit']")
        await asyncio.sleep(1)
        try:
            self.logger.info("Handled we couldn't sign you in.")
            await page.wait_for_selector("We couldn't sign you in", timeout=5000)
            await asyncio.sleep(0.5)
            self.logger.info("Clicking 'Sign in another way'.")
            await page.click("//*[contains(text(),'Sign in another way')]")
            # Use the stable id-based selector for the credential picker link
            await page.wait_for_selector("css=a#idA_PWD_SwitchToCredPicker", timeout=5000)
            await page.click("css=a#idA_PWD_SwitchToCredPicker")
            await asyncio.sleep(0.5)
        except Exception:
            pass

        try:
            self.logger.info("Handling alternative sign-in methods if prompted.")
            await page.wait_for_selector("css=a#idA_PWD_SwitchToCredPicker", timeout=5000)
            await asyncio.sleep(0.5)
            self.logger.info("Clicking 'Sign in another way'.")
            await page.click("//*[contains(text(),'Sign in another way')]")
            #await page.wait_for_selector("//span[contains(text(),'Use your password')]", timeout=5000)
            #await page.click("text=Use your password")
            await asyncio.sleep(0.5)
        except Exception:
            pass
        try:
            await page.click("//span[contains(text(),'Use your password')]")
                    
        except Exception:
            pass

        await page.wait_for_selector("input[type='password']", timeout=10000)
        await page.fill("input[type='password']", self.password)
        await page.click("button:has-text('Next'), input[type='submit']")
        await asyncio.sleep(1)

        try:
            await page.wait_for_selector("button:has-text('Yes'), input[type='submit'][value='Yes']", timeout=5000)
            await page.click("button:has-text('Yes'), input[type='submit'][value='Yes']")
            await asyncio.sleep(1)
        except Exception:
            pass

        await asyncio.sleep(5)
        await page.wait_for_selector('xpath=//h1[contains(text(),"Your family")]', timeout=10000)
        self.logger.info(f"Account sign-in completed for {email}.")
        return True

    async def switch_account_if_needed(self, page: Page, account_name: str = None) -> bool:
        """
        Switches to the requested account if not already logged in.
        Returns True if requested account is active after execution.
        """
        if await self.is_child_account(page, account_name=account_name):
            self.logger.info(f"Requested account '{account_name}' is already active. No switch needed.")
            return True

        self.logger.info(f"Switching account to '{account_name}'.")
        return await self.sign_in_with_different_account(page, account_name=account_name)

    async def switch_family_member(self, page: Page, account_name: str = None) -> bool:
        """
        Clicks the avatar under 'Switch family member', opens the profile/panel for
        that family member, and verifies the displayed email matches the expected
        email for `account_name` (looked up from ACCOUNTS_ID env).

        Returns True on success, False otherwise.
        """
        if not account_name:
            self.logger.error("switch_family_member called without account_name")
            return False

        expected_email = (self.accounts.get(account_name) or "").strip().lower()
        if not expected_email:
            self.logger.error(f"Account '{account_name}' not found in ACCOUNTS_ID environment.")
            return False

        try:
            # Ensure Family page is visible to reliably find the Switch family member area
            try:
                await page.goto("https://account.microsoft.com/family/home", wait_until="networkidle", timeout=10000)
            except Exception:
                # if navigation fails, continue - may already be on the page
                pass

            await asyncio.sleep(0.8)

            # Try to find avatars under "Switch family member" using visible text anchor
            candidates = []
            try:
                candidates = await page.query_selector_all(
                    "xpath=//*[normalize-space()='Switch family member']/following::button | "
                    "//*[normalize-space()='Switch family member']/following::img"
                )
            except Exception:
                candidates = []


            # Fallback: search for avatar buttons/images that may contain account name or an email-like title/aria-label
            if not candidates:
                try:
                    qlist = [
                        f"xpath=//button[contains(translate(@aria-label,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'), '{account_name.lower()}')]",
                        f"xpath=//img[contains(translate(@title,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'), '{account_name.lower()}')]",
                        "xpath=//button[contains(@aria-label, '@')]",
                        "xpath=//img[contains(@title, '@')]",
                    ]
                    for q in qlist:
                        try:
                            found = await page.query_selector_all(q)
                            if found:
                                candidates.extend(found)
                        except Exception:
                            continue
                except Exception:
                    pass

            # Deduplicate handles
            seen = set()
            for handle in candidates:
                try:
                    if not handle:
                        continue
                    uid = await handle.get_attribute('data-uid') or (await handle.get_attribute('aria-label')) or (await handle.get_attribute('title')) or ''
                    if uid in seen:
                        continue
                    seen.add(uid)

                    # Click avatar/button
                    try:
                        await handle.click()
                    except Exception:
                        # try a hover + click fallback
                        try:
                            await handle.hover()
                            await handle.click()
                        except Exception:
                            continue

                    # Wait briefly for profile panel to appear
                    await asyncio.sleep(1.0)

                    # Read displayed account/email using existing helper
                    displayed = await self._get_displayed_account_text(page)
                    if displayed:
                        m = re.search(r"[\w\.-]+@[\w\.-]+", displayed)
                        displayed_email = m.group(0).lower() if m else displayed.strip().lower()
                        if displayed_email == expected_email or expected_email in displayed.lower():
                            self.logger.info(f"Switched to family member '{account_name}' and verified email {expected_email}.")
                            return True

                    # If not matched, try to close any opened menu/panel and continue
                    try:
                        await page.keyboard.press('Escape')
                        await asyncio.sleep(0.25)
                    except Exception:
                        pass

                except Exception:
                    continue

            self.logger.error(f"Could not locate and switch to family member '{account_name}'.")
            return False

        except Exception as e:
            self.logger.error(f"Error in switch_family_member: {e}")
            return False


__all__ = ["AccountService"]
