"""
Complete OAuth 2.0 Flow Implementation for Dynamics 365
Handles browser-based authentication with local redirect server
"""

import aiohttp
import webbrowser
import threading
import socket
import logging
import secrets
from typing import Optional, Callable
from datetime import datetime, timedelta
from flask import Flask, request

logger = logging.getLogger(__name__)


class OAuthRedirectHandler:
    """
    Local server to handle OAuth redirect
    Reused from Salesforce implementation
    """

    def __init__(self, port: int = 8080):
        self.port = port
        self.app = Flask(__name__)
        self.auth_code: Optional[str] = None
        self.error: Optional[str] = None
        self.state: Optional[str] = None
        self._server_thread: Optional[threading.Thread] = None
        self._shutdown_func: Optional[Callable] = None

        # Setup routes
        self.app.route("/callback")(self._handle_callback)

    def _handle_callback(self):
        """Handle OAuth callback"""
        # Extract parameters
        self.auth_code = request.args.get("code")
        self.error = request.args.get("error")
        self.state = request.args.get("state")

        # Return user-friendly response
        if self.auth_code:
            html = """
            <html>
            <head>
                <title>Authentication Successful</title>
                <style>
                    body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
                    .success { color: #4CAF50; }
                    .message { margin: 20px; font-size: 18px; }
                </style>
            </head>
            <body>
                <h1 class="success">✓ Authentication Successful</h1>
                <p class="message">You can now close this window and return to FoundryMatch.</p>
                <script>
                    setTimeout(function() { window.close(); }, 3000);
                </script>
            </body>
            </html>
            """
        else:
            error_msg = self.error or "Unknown error"
            html = f"""
            <html>
            <head>
                <title>Authentication Failed</title>
                <style>
                    body {{ font-family: Arial, sans-serif; text-align: center; padding: 50px; }}
                    .error {{ color: #f44336; }}
                    .message {{ margin: 20px; font-size: 18px; }}
                </style>
            </head>
            <body>
                <h1 class="error">✗ Authentication Failed</h1>
                <p class="message">Error: {error_msg}</p>
                <p>Please close this window and try again.</p>
            </body>
            </html>
            """

        # Schedule shutdown after response
        if self._shutdown_func:
            threading.Timer(0.5, self._shutdown_func).start()

        return html

    def start(self):
        """Start the redirect server"""
        # Find available port if default is taken
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            sock.bind(("", self.port))
            sock.close()
        except OSError:
            # Port is taken, find another
            sock.bind(("", 0))
            self.port = sock.getsockname()[1]
            sock.close()

        # Start Flask in thread
        from werkzeug.serving import make_server

        self._server = make_server("localhost", self.port, self.app)
        self._shutdown_func = self._server.shutdown

        self._server_thread = threading.Thread(
            target=self._server.serve_forever, daemon=True
        )
        self._server_thread.start()

        logger.info(f"OAuth redirect server started on port {self.port}")

    def stop(self):
        """Stop the redirect server"""
        if self._shutdown_func:
            self._shutdown_func()

    def wait_for_code(self, timeout: int = 300) -> Optional[str]:
        """
        Wait for authorization code

        Args:
            timeout: Maximum seconds to wait

        Returns:
            Authorization code or None if timeout/error
        """
        import time

        start_time = time.time()

        while time.time() - start_time < timeout:
            if self.auth_code:
                return self.auth_code
            if self.error:
                raise Exception(f"OAuth error: {self.error}")
            time.sleep(0.5)

        raise TimeoutError("OAuth callback timeout")


class DynamicsOAuthFlow:
    """
    Complete OAuth flow implementation for Dynamics 365
    """

    def __init__(self, integration):
        self.integration = integration
        self.redirect_handler: Optional[OAuthRedirectHandler] = None
        self._state: Optional[str] = None

    async def authenticate_oauth(
        self,
        client_id: str,
        tenant_id: str,
        redirect_uri: Optional[str] = None,
        open_browser: bool = True,
    ) -> bool:
        """
        Complete OAuth 2.0 authentication with Azure AD

        Args:
            client_id: Azure AD application ID
            tenant_id: Azure AD tenant ID
            redirect_uri: OAuth redirect URI (default: http://localhost:8080/callback)
            open_browser: Whether to automatically open the browser

        Returns:
            True if authentication successful
        """
        if redirect_uri is None:
            # Start local redirect server
            self.redirect_handler = OAuthRedirectHandler()
            self.redirect_handler.start()
            redirect_uri = f"http://localhost:{self.redirect_handler.port}/callback"

        # Update credentials
        self.integration.credentials.client_id = client_id
        self.integration.credentials.tenant_id = tenant_id

        # Generate PKCE pair
        self.integration._code_verifier, self.integration._code_challenge = (
            self.integration._generate_pkce_pair()
        )

        # Generate state for CSRF protection
        self._state = secrets.token_urlsafe(32)

        # Build authorization URL
        auth_endpoint = (
            f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize"
        )

        # Determine resource URL if not set
        if not self.integration.credentials.resource:
            # Will be set after successful auth via discovery
            resource = "https://graph.microsoft.com"
        else:
            resource = self.integration.credentials.resource

        scopes = [
            "user.read",
            "openid",
            "profile",
            "offline_access",
            f"{resource}/.default",
        ]

        from urllib.parse import urlencode

        params = {
            "client_id": client_id,
            "response_type": "code",
            "redirect_uri": redirect_uri,
            "scope": " ".join(scopes),
            "state": self._state,
            "code_challenge": self.integration._code_challenge,
            "code_challenge_method": "S256",
            "response_mode": "query",
            "prompt": "select_account",  # Always show account selection
        }

        auth_url = f"{auth_endpoint}?{urlencode(params)}"

        logger.info(f"Starting OAuth flow with redirect URI: {redirect_uri}")

        # Open browser if requested
        if open_browser:
            logger.info("Opening browser for authentication...")
            webbrowser.open(auth_url)
        else:
            logger.info(f"Authorization URL: {auth_url}")

        # Wait for callback if using local server
        if self.redirect_handler:
            try:
                auth_code = self.redirect_handler.wait_for_code(timeout=300)

                # Verify state
                if self.redirect_handler.state != self._state:
                    raise Exception("Invalid state parameter - possible CSRF attack")

                # Exchange code for token
                success = await self.exchange_code_for_token(auth_code, redirect_uri)

                # Clean up
                self.redirect_handler.stop()

                return success

            except Exception as e:
                logger.error(f"OAuth flow failed: {str(e)}")
                if self.redirect_handler:
                    self.redirect_handler.stop()
                return False
        else:
            # Caller will handle the callback
            return True

    async def exchange_code_for_token(self, auth_code: str, redirect_uri: str) -> bool:
        """
        Exchange authorization code for access token

        Args:
            auth_code: Authorization code from callback
            redirect_uri: Redirect URI used in authorization request

        Returns:
            True if token exchange successful
        """
        token_endpoint = (
            f"https://login.microsoftonline.com/"
            f"{self.integration.credentials.tenant_id}/oauth2/v2.0/token"
        )

        # Prepare token request
        data = {
            "client_id": self.integration.credentials.client_id,
            "grant_type": "authorization_code",
            "code": auth_code,
            "redirect_uri": redirect_uri,
            "code_verifier": self.integration._code_verifier,
        }

        # Add client secret if available
        if self.integration.credentials.client_secret:
            data["client_secret"] = self.integration.credentials.client_secret

        # Add scope if resource is known
        if self.integration.credentials.resource:
            data["scope"] = (
                f"{self.integration.credentials.resource}/.default offline_access"
            )

        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(token_endpoint, data=data) as resp:
                    if resp.status == 200:
                        token_data = await resp.json()

                        # Store tokens
                        self.integration.credentials.access_token = token_data[
                            "access_token"
                        ]
                        self.integration.credentials.refresh_token = token_data.get(
                            "refresh_token"
                        )

                        # Calculate token expiry
                        expires_in = token_data.get("expires_in", 3600)
                        self.integration.credentials.token_expires_at = (
                            datetime.utcnow() + timedelta(seconds=expires_in)
                        )

                        # Discover Dynamics instance if not set
                        if not self.integration.credentials.resource:
                            await self._discover_dynamics_instance()

                        logger.info("Token exchange successful")
                        return True
                    else:
                        error_data = await resp.json()
                        error = error_data.get("error", "unknown_error")
                        error_desc = error_data.get(
                            "error_description", "Token exchange failed"
                        )
                        logger.error(f"Token exchange failed: {error} - {error_desc}")
                        return False

        except Exception as e:
            logger.error(f"Token exchange error: {str(e)}")
            return False

    async def _discover_dynamics_instance(self):
        """
        Discover user's Dynamics 365 instance using global discovery
        """
        discovery_url = (
            "https://globaldisco.crm.dynamics.com/api/discovery/v2.0/Instances"
        )

        headers = {
            "Authorization": f"Bearer {self.integration.credentials.access_token}",
            "Accept": "application/json",
        }

        try:
            async with aiohttp.ClientSession() as session:
                async with session.get(discovery_url, headers=headers) as resp:
                    if resp.status == 200:
                        data = await resp.json()
                        instances = data.get("value", [])

                        if instances:
                            # Use first instance (production)
                            instance = instances[0]
                            self.integration.credentials.resource = instance["ApiUrl"]
                            logger.info(
                                f"Discovered Dynamics instance: {instance['FriendlyName']}"
                            )
                        else:
                            logger.warning("No Dynamics instances found for user")

        except Exception as e:
            logger.error(f"Instance discovery failed: {str(e)}")
