import requests
import os
import time
from typing import List, Dict, Any, Optional
import pandas as pd
from dotenv import load_dotenv
from rich.console import Console
from rich.table import Table
from rich.prompt import Prompt, Confirm
from collections import deque
from datetime import datetime, timedelta

# Load environment variables from .env file
load_dotenv()
APOLLO_API_KEY = os.getenv("APOLLO_API_KEY")
console = Console()


class RateLimiter:
    """Rate limiter to enforce API request limits."""

    def __init__(self, minute_limit: int, hourly_limit: int, daily_limit: int):
        """Initialize rate limiter with specified limits."""
        self.minute_limit = minute_limit
        self.hourly_limit = hourly_limit
        self.daily_limit = daily_limit

        # Queues to track request timestamps
        self.minute_requests = deque()
        self.hourly_requests = deque()
        self.daily_requests = deque()

    def _clean_old_requests(self):
        """Remove expired requests from tracking queues."""
        now = datetime.now()

        # Clean minute requests older than 1 minute
        minute_ago = now - timedelta(minutes=1)
        while self.minute_requests and self.minute_requests[0] < minute_ago:
            self.minute_requests.popleft()

        # Clean hourly requests older than 1 hour
        hour_ago = now - timedelta(hours=1)
        while self.hourly_requests and self.hourly_requests[0] < hour_ago:
            self.hourly_requests.popleft()

        # Clean daily requests older than 1 day
        day_ago = now - timedelta(days=1)
        while self.daily_requests and self.daily_requests[0] < day_ago:
            self.daily_requests.popleft()

    def check_and_wait(self):
        """
        Check if request can be made within rate limits.
        If not, wait until it's safe to make the request.
        """
        while True:
            self._clean_old_requests()

            # Check if any limits are exceeded
            if (
                len(self.minute_requests) >= self.minute_limit
                or len(self.hourly_requests) >= self.hourly_limit
                or len(self.daily_requests) >= self.daily_limit
            ):

                # Calculate wait times for each limit
                wait_times = []

                if len(self.minute_requests) >= self.minute_limit:
                    # Wait until oldest request in minute window expires
                    wait_time = (
                        self.minute_requests[0] + timedelta(minutes=1) - datetime.now()
                    ).total_seconds()
                    wait_times.append(max(0, wait_time))

                if len(self.hourly_requests) >= self.hourly_limit:
                    # Wait until oldest request in hourly window expires
                    wait_time = (
                        self.hourly_requests[0] + timedelta(hours=1) - datetime.now()
                    ).total_seconds()
                    wait_times.append(max(0, wait_time))

                if len(self.daily_requests) >= self.daily_limit:
                    # Wait until oldest request in daily window expires
                    wait_time = (
                        self.daily_requests[0] + timedelta(days=1) - datetime.now()
                    ).total_seconds()
                    wait_times.append(max(0, wait_time))

                if wait_times:
                    # Wait for the shortest time needed to be under a limit
                    wait_time = min(wait_times)
                    if wait_time > 0:
                        console.print(
                            f"[yellow]Rate limit approaching. Waiting {wait_time:.2f} seconds...[/yellow]"
                        )
                        time.sleep(wait_time)
                        continue  # Recheck after waiting

            # If we get here, we're under all limits
            break

    def record_request(self):
        """Record a request being made."""
        now = datetime.now()
        self.minute_requests.append(now)
        self.hourly_requests.append(now)
        self.daily_requests.append(now)


class ApolloClient:
    """Client for interacting with the Apollo.io API."""

    BASE_URL = "https://api.apollo.io/v1"

    def __init__(self, api_key: Optional[str] = None):
        """Initialize the Apollo client with API key."""
        self.api_key = api_key or APOLLO_API_KEY
        if not self.api_key:
            raise ValueError(
                "Apollo API key is required. Set it as APOLLO_API_KEY in your environment or .env file."
            )

        self.headers = {
            "Content-Type": "application/json",
            "Cache-Control": "no-cache",
            "X-API-Key": self.api_key,
        }

        # Initialize rate limiter with specified limits
        self.rate_limiter = RateLimiter(
            minute_limit=200,  # 200 requests per minute
            hourly_limit=4000,  # 4000 requests per hour
            daily_limit=2000,  # 2000 requests per day
        )

    def _make_request(
        self, method: str, endpoint: str, payload: Dict[str, Any]
    ) -> Dict[str, Any]:
        """
        Make an API request with rate limiting.

        Args:
            method: HTTP method (POST, GET, etc.)
            endpoint: API endpoint
            payload: Request payload

        Returns:
            API response as JSON
        """
        # Check rate limits and wait if necessary
        self.rate_limiter.check_and_wait()

        # Make the request
        if method.upper() == "POST":
            response = requests.post(
                endpoint, headers=self.headers, json=payload, timeout=10
            )
        elif method.upper() == "GET":
            response = requests.get(
                endpoint, headers=self.headers, params=payload, timeout=10
            )
        else:
            raise ValueError(f"Unsupported HTTP method: {method}")

        # Record the request
        self.rate_limiter.record_request()

        # Check for rate limit headers in response
        remaining_minute = response.headers.get("X-RateLimit-Minute-Remaining")
        remaining_hour = response.headers.get("X-RateLimit-Hour-Remaining")
        remaining_day = response.headers.get("X-RateLimit-Day-Remaining")

        if remaining_minute is not None:
            console.print(
                f"[dim]Remaining API calls: {remaining_minute}/minute, {remaining_hour}/hour, {remaining_day}/day[/dim]"
            )

        # Handle response
        response.raise_for_status()
        return response.json()

    def search_organizations(
        self, query: str, page: int = 1, per_page: int = 25
    ) -> Dict[str, Any]:
        """
        Search for organizations in Apollo.io.

        Args:
            query: Search query for organization name
            page: Page number for pagination
            per_page: Number of results per page

        Returns:
            Dictionary containing search results
        """
        endpoint = f"{self.BASE_URL}/organizations/search"
        payload = {"q_organization_name": query, "page": page, "per_page": per_page}
        return self._make_request("POST", endpoint, payload)

    def search_people(
        self, organization_id: str, page: int = 1, per_page: int = 25
    ) -> Dict[str, Any]:
        """
        Search for people at a specific organization.

        Args:
            organization_id: Apollo.io organization ID
            page: Page number for pagination
            per_page: Number of results per page

        Returns:
            Dictionary containing people search results
        """
        endpoint = f"{self.BASE_URL}/people/search"
        payload = {
            "organization_ids": [organization_id],
            "page": page,
            "per_page": per_page,
        }
        return self._make_request("POST", endpoint, payload)

    def enrich_person(self, person_id: str) -> Dict[str, Any]:
        """
        Get detailed information for a specific person.

        Args:
            person_id: Apollo.io person ID

        Returns:
            Dictionary containing enriched person data
        """
        endpoint = f"{self.BASE_URL}/people/enrich"
        payload = {"id": person_id}
        return self._make_request("POST", endpoint, payload)

    def bulk_enrich_people(self, person_ids: List[str]) -> Dict[str, Any]:
        """
        Get detailed information for multiple people.

        Args:
            person_ids: List of Apollo.io person IDs

        Returns:
            Dictionary containing enriched people data
        """
        endpoint = f"{self.BASE_URL}/people/bulk_enrich"
        payload = {"ids": person_ids}
        return self._make_request("POST", endpoint, payload)


def display_organizations(organizations: List[Dict[str, Any]]) -> int:
    """
    Display a table of organizations and prompt user to select one.

    Args:
        organizations: List of organization data

    Returns:
        Index of selected organization
    """
    table = Table(title="Organizations")

    table.add_column("#", style="cyan")
    table.add_column("Name", style="green")
    table.add_column("Website", style="blue")
    table.add_column("Industry", style="yellow")
    table.add_column("Size", style="magenta")

    for i, org in enumerate(organizations, 1):
        table.add_row(
            str(i),
            org.get("name", "N/A"),
            org.get("website_url", "N/A"),
            org.get("industry", "N/A"),
            org.get("employee_count", "N/A"),
        )

    console.print(table)

    while True:
        try:
            selection = int(Prompt.ask("Select an organization (number)"))
            if 1 <= selection <= len(organizations):
                return selection - 1
            console.print("[bold red]Invalid selection. Please try again.[/bold red]")
        except ValueError:
            console.print("[bold red]Please enter a valid number.[/bold red]")


def display_people(people: List[Dict[str, Any]]) -> List[int]:
    """
    Display a table of people and prompt user to select multiple.

    Args:
        people: List of people data

    Returns:
        List of indices of selected people
    """
    table = Table(title="People")

    table.add_column("#", style="cyan")
    table.add_column("Name", style="green")
    table.add_column("Title", style="blue")
    table.add_column("Seniority", style="yellow")
    table.add_column("Department", style="magenta")

    for i, person in enumerate(people, 1):
        table.add_row(
            str(i),
            f"{person.get('first_name', '')} {person.get('last_name', '')}",
            person.get("title", "N/A"),
            person.get("seniority", "N/A"),
            person.get("department", "N/A"),
        )

    console.print(table)

    selections = []
    while True:
        selection_input = Prompt.ask(
            "Select people (comma-separated numbers, or 'all' for all)"
        )

        if selection_input.lower() == "all":
            return list(range(len(people)))

        try:
            selections = [int(x.strip()) - 1 for x in selection_input.split(",")]
            if all(0 <= sel < len(people) for sel in selections):
                return selections
            console.print(
                "[bold red]Some selections are out of range. Please try again.[/bold red]"
            )
        except ValueError:
            console.print(
                "[bold red]Please enter valid numbers separated by commas.[/bold red]"
            )


def display_enriched_people(people: List[Dict[str, Any]], export: bool = False) -> None:
    """
    Display detailed information for enriched people.

    Args:
        people: List of enriched people data
        export: Whether to export data to CSV
    """
    table = Table(title="Contact Information")

    table.add_column("Name", style="green")
    table.add_column("Title", style="blue")
    table.add_column("Email", style="yellow")
    table.add_column("Phone", style="magenta")
    table.add_column("LinkedIn", style="cyan")

    data_for_export = []

    for person in people:
        person_data = person.get("person", {})
        name = f"{person_data.get('first_name', '')} {person_data.get('last_name', '')}"
        title = person_data.get("title", "N/A")
        email = person_data.get("email", "N/A")
        phone = person_data.get("phone_number", "N/A")
        linkedin = person_data.get("linkedin_url", "N/A")

        table.add_row(name, title, email, phone, linkedin)

        if export:
            data_for_export.append(
                {
                    "Name": name,
                    "Title": title,
                    "Email": email,
                    "Phone": phone,
                    "LinkedIn": linkedin,
                    "Company": person_data.get("organization", {}).get("name", "N/A"),
                    "Department": person_data.get("department", "N/A"),
                    "Seniority": person_data.get("seniority", "N/A"),
                }
            )

    console.print(table)

    if export and data_for_export:
        df = pd.DataFrame(data_for_export)
        filename = f"apollo_contacts_{int(time.time())}.csv"
        df.to_csv(filename, index=False)
        console.print(f"[bold green]Data exported to {filename}[/bold green]")


def main():
    """Main function to run the Apollo.io contact finder."""
    console.print("[bold]Apollo.io Contact Finder[/bold]", style="blue")

    try:
        client = ApolloClient()

        # Search for organizations
        query = Prompt.ask("Enter company name to search")

        with console.status("[bold green]Searching for organizations...[/bold green]"):
            org_results = client.search_organizations(query)

        organizations = org_results.get("organizations", [])

        if not organizations:
            console.print(
                "[bold red]No organizations found. Please try a different search.[/bold red]"
            )
            return

        # Display organizations and get selection
        selected_org_idx = display_organizations(organizations)
        selected_org = organizations[selected_org_idx]

        console.print(f"[bold green]Selected: {selected_org['name']}[/bold green]")

        # Search for people at the selected organization
        with console.status("[bold green]Searching for people...[/bold green]"):
            people_results = client.search_people(selected_org["id"])

        people = people_results.get("people", [])

        if not people:
            console.print("[bold red]No people found at this organization.[/bold red]")
            return

        # Display people and get selections
        selected_people_idx = display_people(people)
        selected_people = [people[idx] for idx in selected_people_idx]

        if not selected_people:
            console.print("[bold yellow]No people selected. Exiting.[/bold yellow]")
            return

        # Get detailed information for selected people
        person_ids = [person["id"] for person in selected_people]

        with console.status(
            "[bold green]Getting detailed contact information...[/bold green]"
        ):
            if len(person_ids) == 1:
                enriched_results = {
                    "person": client.enrich_person(person_ids[0]).get("person", {})
                }
                enriched_people = [enriched_results]
            else:
                enriched_results = client.bulk_enrich_people(person_ids)
                enriched_people = enriched_results.get("people", [])

        # Display enriched people information
        export_to_csv = Confirm.ask("Export results to CSV?")
        display_enriched_people(enriched_people, export=export_to_csv)

    except requests.exceptions.RequestException as e:
        console.print(f"[bold red]API Error: {str(e)}[/bold red]")
    except Exception as e:
        console.print(f"[bold red]Error: {str(e)}[/bold red]")


if __name__ == "__main__":
    main()
