"""
HubSpot integration core module for FoundryOps FuzzyMatcher.
Handles OAuth 2.0 with PKCE, API requests, rate limiting, and metadata retrieval.
"""

import asyncio
import base64
import hashlib
import logging
import secrets
import time
import webbrowser
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urlencode, urlparse, parse_qs

import aiohttp
from aiohttp import ClientSession, ClientTimeout

logger = logging.getLogger(__name__)


@dataclass
class HubSpotCredentials:
    """HubSpot OAuth credentials and metadata."""

    client_id: str
    client_secret: str = ""  # Empty for public apps using PKCE
    redirect_uri: str = "http://localhost:8089/callback"
    refresh_token: Optional[str] = None
    access_token: Optional[str] = None
    expires_at: Optional[float] = None
    portal_id: Optional[str] = None
    user_email: Optional[str] = None
    use_private_app: bool = False  # Toggle for private app token mode


@dataclass
class ApiLimits:
    """Track HubSpot API rate limits across different buckets."""

    core_api_remaining: int = 150
    core_api_max: int = 150
    search_api_remaining: int = 10
    search_api_max: int = 10
    batch_api_remaining: int = 100
    batch_api_max: int = 100
    daily_remaining: Optional[int] = None
    daily_max: Optional[int] = None
    reset_at: Optional[float] = None


@dataclass
class HubSpotObject:
    """Metadata for a HubSpot object type."""

    name: str
    label: str
    custom: bool = False
    searchable: bool = True
    properties_url: Optional[str] = None


@dataclass
class HubSpotProperty:
    """Metadata for a HubSpot property/field."""

    name: str
    label: str
    type: str  # string, number, date, datetime, enumeration, etc.
    field_type: str  # text, textarea, select, checkbox, etc.
    required: bool = False
    read_only: bool = False
    max_length: Optional[int] = None
    options: List[Dict[str, str]] = field(default_factory=list)
    description: Optional[str] = None


class HubSpotIntegration:
    """Main integration class for HubSpot API operations."""

    BASE_URL = "https://api.hubapi.com"
    AUTH_URL = "https://app.hubspot.com/oauth/authorize"
    TOKEN_URL = "https://api.hubapi.com/oauth/v1/token"

    def __init__(self, credentials: HubSpotCredentials):
        self.credentials = credentials
        self.session: Optional[ClientSession] = None
        self.api_limits = ApiLimits()
        self._rate_limiter = asyncio.Semaphore(150)  # 150 requests per 10 seconds
        self._rate_limit_reset_task: Optional[asyncio.Task] = None
        self._property_cache: Dict[str, List[HubSpotProperty]] = {}

    async def __aenter__(self):
        """Async context manager entry."""
        await self.connect()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit."""
        await self.disconnect()

    async def connect(self):
        """Initialize the HTTP session with retry configuration."""
        if self.session:
            await self.session.close()

        timeout = ClientTimeout(total=30, connect=10)
        connector = aiohttp.TCPConnector(limit=20, force_close=True)

        self.session = ClientSession(
            timeout=timeout,
            connector=connector,
            headers={
                "User-Agent": "FoundryOps-FuzzyMatcher/1.0",
                "Accept": "application/json",
                "Content-Type": "application/json",
            },
        )

        # Start rate limit reset task
        if not self._rate_limit_reset_task:
            self._rate_limit_reset_task = asyncio.create_task(
                self._rate_limit_reset_loop()
            )

    async def disconnect(self):
        """Close the HTTP session and cleanup."""
        if self._rate_limit_reset_task:
            self._rate_limit_reset_task.cancel()
            try:
                await self._rate_limit_reset_task
            except asyncio.CancelledError:
                pass

        if self.session:
            await self.session.close()
            self.session = None

    async def _rate_limit_reset_loop(self):
        """Reset rate limiter every 10 seconds."""
        while True:
            await asyncio.sleep(10)
            # Reset semaphore by creating a new one
            self._rate_limiter = asyncio.Semaphore(150)

    def is_expired(self) -> bool:
        """Check if the access token is expired."""
        if self.credentials.use_private_app:
            return False  # Private app tokens don't expire

        if not self.credentials.expires_at:
            return True

        # Add 5 minute buffer
        return time.time() > (self.credentials.expires_at - 300)

    async def authenticate_oauth(self, port: int = 8089) -> bool:
        """
        Perform OAuth 2.0 authentication with PKCE.
        Returns True if successful.
        """
        if self.credentials.use_private_app:
            # Private app mode - token should already be set
            return bool(self.credentials.access_token)

        # Generate PKCE challenge
        code_verifier = (
            base64.urlsafe_b64encode(secrets.token_bytes(32))
            .decode("utf-8")
            .rstrip("=")
        )
        code_challenge = (
            base64.urlsafe_b64encode(
                hashlib.sha256(code_verifier.encode("utf-8")).digest()
            )
            .decode("utf-8")
            .rstrip("=")
        )

        # Build authorization URL
        auth_params = {
            "client_id": self.credentials.client_id,
            "redirect_uri": self.credentials.redirect_uri,
            "scope": " ".join(
                [
                    "crm.objects.contacts.read",
                    "crm.objects.contacts.write",
                    "crm.objects.companies.read",
                    "crm.objects.companies.write",
                    "crm.objects.deals.read",
                    "crm.objects.deals.write",
                    "crm.schemas.contacts.read",
                    "crm.schemas.companies.read",
                    "crm.schemas.deals.read",
                    "crm.schemas.custom.read",
                    "crm.import",
                    "crm.objects.owners.read",
                    "offline",
                ]
            ),
            "response_type": "code",
            "code_challenge": code_challenge,
            "code_challenge_method": "S256",
        }

        auth_url = f"{self.AUTH_URL}?{urlencode(auth_params)}"

        # Start local callback server
        from http.server import HTTPServer, BaseHTTPRequestHandler
        import threading

        auth_code = None
        auth_error = None

        class CallbackHandler(BaseHTTPRequestHandler):
            def do_GET(self):
                nonlocal auth_code, auth_error

                parsed = urlparse(self.path)
                params = parse_qs(parsed.query)

                if "code" in params:
                    auth_code = params["code"][0]
                    self.send_response(200)
                    self.send_header("Content-type", "text/html")
                    self.end_headers()
                    self.wfile.write(
                        b"<html><body><h1>Authorization successful!</h1><p>You can close this window.</p></body></html>"
                    )
                else:
                    auth_error = params.get("error", ["Unknown error"])[0]
                    self.send_response(400)
                    self.send_header("Content-type", "text/html")
                    self.end_headers()
                    self.wfile.write(
                        f"<html><body><h1>Authorization failed</h1><p>Error: {auth_error}</p></body></html>".encode()
                    )

            def log_message(self, format, *args):
                pass  # Suppress logs

        server = HTTPServer(("localhost", port), CallbackHandler)
        server_thread = threading.Thread(target=server.serve_forever)
        server_thread.daemon = True
        server_thread.start()

        # Open browser
        logger.info(f"Opening browser for HubSpot OAuth: {auth_url}")
        webbrowser.open(auth_url)

        # Wait for callback (timeout after 5 minutes)
        timeout = time.time() + 300
        while auth_code is None and auth_error is None and time.time() < timeout:
            await asyncio.sleep(0.5)

        server.shutdown()

        if auth_error:
            logger.error(f"OAuth error: {auth_error}")
            return False

        if not auth_code:
            logger.error("OAuth timeout - no authorization code received")
            return False

        # Exchange code for tokens
        token_data = {
            "grant_type": "authorization_code",
            "client_id": self.credentials.client_id,
            "redirect_uri": self.credentials.redirect_uri,
            "code": auth_code,
            "code_verifier": code_verifier,
        }

        if self.credentials.client_secret:
            token_data["client_secret"] = self.credentials.client_secret

        try:
            async with self.session.post(self.TOKEN_URL, data=token_data) as response:
                if response.status == 200:
                    data = await response.json()
                    self.credentials.access_token = data["access_token"]
                    self.credentials.refresh_token = data["refresh_token"]
                    self.credentials.expires_at = time.time() + data["expires_in"]

                    # Get portal info
                    await self._get_portal_info()
                    return True
                else:
                    error_text = await response.text()
                    logger.error(
                        f"Token exchange failed: {response.status} - {error_text}"
                    )
                    return False

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

    async def refresh_token(self) -> bool:
        """Refresh the access token using the refresh token."""
        if self.credentials.use_private_app:
            return True  # Private app tokens don't need refresh

        if not self.credentials.refresh_token:
            logger.error("No refresh token available")
            return False

        token_data = {
            "grant_type": "refresh_token",
            "client_id": self.credentials.client_id,
            "refresh_token": self.credentials.refresh_token,
        }

        if self.credentials.client_secret:
            token_data["client_secret"] = self.credentials.client_secret

        try:
            async with self.session.post(self.TOKEN_URL, data=token_data) as response:
                if response.status == 200:
                    data = await response.json()
                    self.credentials.access_token = data["access_token"]
                    self.credentials.refresh_token = data["refresh_token"]
                    self.credentials.expires_at = time.time() + data["expires_in"]
                    logger.info("Token refreshed successfully")
                    return True
                else:
                    error_text = await response.text()
                    logger.error(
                        f"Token refresh failed: {response.status} - {error_text}"
                    )
                    return False

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

    async def _get_portal_info(self):
        """Get portal ID and user email from access token."""
        try:
            result, _ = await self._execute_api_request("GET", "/integrations/v1/me")
            if result:
                self.credentials.portal_id = str(result.get("portalId", ""))
                self.credentials.user_email = result.get("user", "")

        except Exception as e:
            logger.warning(f"Could not get portal info: {e}")

    async def _execute_api_request(
        self, method: str, endpoint: str, **kwargs
    ) -> Tuple[Optional[Dict[str, Any]], int]:
        """
        Execute an API request with rate limiting and retry logic.
        Returns (json_response, status_code).
        """
        if not self.session:
            await self.connect()

        # Check token expiry
        if self.is_expired():
            if not await self.refresh_token():
                # Audit failed auth
                await self._audit_api_call(
                    endpoint,
                    method,
                    401,
                    kwargs.get("json"),
                    None,
                    "Token refresh failed",
                )
                return None, 401

        # Ensure we have auth header
        headers = kwargs.get("headers", {})
        headers["Authorization"] = f"Bearer {self.credentials.access_token}"
        kwargs["headers"] = headers

        # CRITICAL FIX: Remove Content-Type for multipart uploads
        if "data" in kwargs:
            from aiohttp import FormData

            if isinstance(kwargs["data"], FormData):
                headers.pop("Content-Type", None)  # Let aiohttp set the boundary
                # Prevent session default from being applied
                kwargs.setdefault("skip_auto_headers", set()).add("Content-Type")

        # Prepare URL
        url = endpoint if endpoint.startswith("http") else f"{self.BASE_URL}{endpoint}"

        # Rate limiting
        async with self._rate_limiter:
            max_retries = 3
            retry_count = 0
            last_error = None

            while retry_count < max_retries:
                try:
                    async with self.session.request(method, url, **kwargs) as response:
                        # Update rate limits from headers
                        self._update_rate_limits(response.headers)

                        # Handle different status codes
                        if response.status == 200:
                            result = await response.json()
                            # Audit successful call
                            await self._audit_api_call(
                                endpoint, method, 200, kwargs.get("json"), result, None
                            )
                            return result, response.status
                        elif response.status == 429:  # Rate limited
                            retry_after = int(response.headers.get("Retry-After", "10"))
                            logger.warning(f"Rate limited, waiting {retry_after}s")
                            await asyncio.sleep(retry_after)
                            retry_count += 1
                            continue
                        elif response.status in [401, 403]:  # Auth issues
                            if (
                                response.status == 401
                                and not self.credentials.use_private_app
                            ):
                                # Try refresh once
                                if retry_count == 0 and await self.refresh_token():
                                    retry_count += 1
                                    continue
                            error_data = await response.json()
                            logger.error(f"Auth error: {error_data}")
                            # Audit auth error
                            await self._audit_api_call(
                                endpoint,
                                method,
                                response.status,
                                kwargs.get("json"),
                                None,
                                str(error_data),
                            )
                            return error_data, response.status
                        else:
                            error_data = await response.json()
                            logger.error(f"API error {response.status}: {error_data}")
                            # Audit API error
                            await self._audit_api_call(
                                endpoint,
                                method,
                                response.status,
                                kwargs.get("json"),
                                None,
                                str(error_data),
                            )
                            return error_data, response.status

                except asyncio.TimeoutError:
                    logger.error(f"Request timeout: {method} {url}")
                    last_error = "Request timeout"
                    retry_count += 1
                    if retry_count < max_retries:
                        await asyncio.sleep(2**retry_count)
                    continue
                except Exception as e:
                    logger.error(f"Request error: {e}")
                    last_error = str(e)
                    retry_count += 1
                    if retry_count < max_retries:
                        await asyncio.sleep(2**retry_count)
                    continue

            # All retries exhausted
            await self._audit_api_call(
                endpoint,
                method,
                500,
                kwargs.get("json"),
                None,
                last_error or "Max retries exceeded",
            )
            return None, 500

    async def _audit_api_call(
        self,
        endpoint: str,
        method: str,
        status_code: int,
        request_data: Optional[Dict[str, Any]],
        response_data: Optional[Dict[str, Any]],
        error: Optional[str],
    ):
        """Helper method to audit API calls."""
        if hasattr(self, "audit_logger") and self.audit_logger:
            try:
                await self.audit_logger.audit_api_call(
                    endpoint=endpoint,
                    method=method,
                    status_code=status_code,
                    request_data=request_data,
                    response_data=response_data,
                    error=error,
                )
            except Exception as e:
                logger.warning(f"Failed to audit API call: {e}")

    def _update_rate_limits(self, headers: Dict[str, str]):
        """Update rate limit tracking from response headers."""
        # HubSpot uses different header names than standard
        if "X-HubSpot-RateLimit-Secondly-Remaining" in headers:
            self.api_limits.core_api_remaining = int(
                headers.get("X-HubSpot-RateLimit-Secondly-Remaining", 0)
            )
        if "X-HubSpot-RateLimit-Secondly" in headers:
            self.api_limits.core_api_max = int(
                headers.get("X-HubSpot-RateLimit-Secondly", 150)
            )

        # Daily limits (may be absent on dev portals)
        if "X-HubSpot-RateLimit-Daily-Remaining" in headers:
            self.api_limits.daily_remaining = int(
                headers.get("X-HubSpot-RateLimit-Daily-Remaining", 0)
            )
        if "X-HubSpot-RateLimit-Daily" in headers:
            self.api_limits.daily_max = int(headers.get("X-HubSpot-RateLimit-Daily", 0))

    async def get_objects(self) -> List[HubSpotObject]:
        """Get list of available HubSpot object types."""
        # Start with standard objects
        standard_objects = [
            HubSpotObject(
                name="contacts",
                label="Contacts",
                custom=False,
                properties_url="/crm/v3/properties/contacts",
            ),
            HubSpotObject(
                name="companies",
                label="Companies",
                custom=False,
                properties_url="/crm/v3/properties/companies",
            ),
            HubSpotObject(
                name="deals",
                label="Deals",
                custom=False,
                properties_url="/crm/v3/properties/deals",
            ),
            HubSpotObject(
                name="tickets",
                label="Tickets",
                custom=False,
                properties_url="/crm/v3/properties/tickets",
            ),
        ]

        # Try to get custom objects
        try:
            result, status = await self._execute_api_request("GET", "/crm/v3/schemas")

            if status == 200 and result and "results" in result:
                for schema in result["results"]:
                    if schema.get("objectTypeId", "").startswith(
                        "2-"
                    ):  # Custom objects
                        obj = HubSpotObject(
                            name=schema["name"],
                            label=schema.get("labels", {}).get(
                                "plural", schema["name"]
                            ),
                            custom=True,
                            properties_url=f"/crm/v3/properties/{schema['name']}",
                        )
                        standard_objects.append(obj)

        except Exception as e:
            logger.warning(f"Could not fetch custom objects: {e}")

        return standard_objects

    async def get_object_properties(self, object_name: str) -> List[HubSpotProperty]:
        """Get property definitions for a HubSpot object."""
        # Check cache first
        if object_name in self._property_cache:
            return self._property_cache[object_name]

        properties = []

        try:
            result, status = await self._execute_api_request(
                "GET", f"/crm/v3/properties/{object_name}"
            )

            if status == 200 and result and "results" in result:
                for prop_data in result["results"]:
                    prop = HubSpotProperty(
                        name=prop_data["name"],
                        label=prop_data.get("label", prop_data["name"]),
                        type=prop_data.get("type", "string"),
                        field_type=prop_data.get("fieldType", "text"),
                        required=prop_data.get("formField", False),
                        read_only=prop_data.get("modificationMetadata", {}).get(
                            "readOnlyValue", False
                        ),
                        max_length=prop_data.get("maxLength"),  # Use correct field
                        description=prop_data.get("description", ""),
                        options=[
                            {
                                "label": opt.get("label", ""),
                                "value": opt.get("value", ""),
                            }
                            for opt in prop_data.get("options", [])
                        ]
                        if prop_data.get("type") == "enumeration"
                        else [],
                    )
                    properties.append(prop)

                # Cache the results
                self._property_cache[object_name] = properties

        except Exception as e:
            logger.error(f"Error fetching properties for {object_name}: {e}")

        return properties

    async def _paged_get(
        self, url: str, params: Optional[Dict[str, Any]] = None, max_pages: int = 100
    ) -> List[Dict[str, Any]]:
        """
        Helper for paginated GET requests.
        HubSpot uses 'after' token for pagination.
        """
        if params is None:
            params = {}

        all_results = []
        page_count = 0
        after = None

        while page_count < max_pages:
            if after:
                params["after"] = after

            result, status = await self._execute_api_request("GET", url, params=params)

            if status != 200 or not result:
                break

            results = result.get("results", [])
            all_results.extend(results)

            # Check for next page
            paging = result.get("paging", {})
            if "next" in paging and "after" in paging["next"]:
                after = paging["next"]["after"]
                page_count += 1
            else:
                break

        return all_results

    async def batch_create(
        self,
        object_name: str,
        records: List[Dict[str, Any]],
        progress_callback: Optional[callable] = None,
    ) -> Dict[str, Any]:
        """Batch create records using HubSpot batch API."""
        endpoint = f"/crm/v3/objects/{object_name}/batch/create"

        # HubSpot batch limit is 100
        batch_size = 100
        total_created = 0
        errors = []

        for i in range(0, len(records), batch_size):
            batch = records[i : i + batch_size]

            if progress_callback:
                progress = (i / len(records)) * 100
                progress_callback(
                    f"Creating {object_name} batch {i//batch_size + 1}", progress
                )

            payload = {"inputs": [{"properties": record} for record in batch]}

            result, status = await self._execute_api_request(
                "POST", endpoint, json=payload
            )

            if status == 200 and result:
                total_created += len(result.get("results", []))
            else:
                errors.append({"batch_index": i // batch_size, "error": result})

        return {"created": total_created, "errors": errors}

    async def batch_update(
        self,
        object_name: str,
        records: List[Dict[str, Any]],
        progress_callback: Optional[callable] = None,
    ) -> Dict[str, Any]:
        """Batch update records using HubSpot batch API."""
        endpoint = f"/crm/v3/objects/{object_name}/batch/update"

        batch_size = 100
        total_updated = 0
        errors = []

        for i in range(0, len(records), batch_size):
            batch = records[i : i + batch_size]

            if progress_callback:
                progress = (i / len(records)) * 100
                progress_callback(
                    f"Updating {object_name} batch {i//batch_size + 1}", progress
                )

            payload = {
                "inputs": [
                    {
                        "id": record.pop("id"),  # ID must be separate
                        "properties": record,
                    }
                    for record in batch
                ]
            }

            result, status = await self._execute_api_request(
                "POST", endpoint, json=payload
            )

            if status == 200 and result:
                total_updated += len(result.get("results", []))
            else:
                errors.append({"batch_index": i // batch_size, "error": result})

        return {"updated": total_updated, "errors": errors}

    async def associate_objects(
        self,
        from_object: str,
        from_id: str,
        to_object: str,
        to_id: str,
        association_type: str = "default",
    ) -> bool:
        """Create association between two objects."""
        endpoint = f"/crm/v3/objects/{from_object}/{from_id}/associations/{to_object}/{to_id}/{association_type}"

        result, status = await self._execute_api_request("PUT", endpoint)

        return status == 200
