import os
import re
import time
from typing import Any, Dict, List, Optional, Set

from dotenv import load_dotenv
from flowsint_core.core.enricher_base import Enricher
from flowsint_core.core.logger import Logger
from flowsint_types.address import Location
from flowsint_types.domain import Domain
from flowsint_types.email import Email
from flowsint_types.individual import Individual
from flowsint_types.organization import Organization
from flowsint_types.phone import Phone

from flowsint_enrichers.registry import flowsint_enricher
from tools.network.whoxy import WhoxyTool

load_dotenv()


@flowsint_enricher
class DomainToHistoryEnricher(Enricher):
    """[WHOXY] Takes a domain and returns history infos about it (history, organization, owners, emails, etc.)."""

    # Define types as class attributes - base class handles schema generation automatically
    InputType = Domain
    OutputType = Domain

    def __init__(
        self,
        sketch_id: Optional[str] = None,
        scan_id: Optional[str] = None,
        vault=None,
        params: Optional[Dict[str, Any]] = None,
    ):
        super().__init__(
            sketch_id=sketch_id,
            scan_id=scan_id,
            params_schema=self.get_params_schema(),
            vault=vault,
            params=params,
        )

    @classmethod
    def required_params(cls) -> bool:
        return True

    @classmethod
    def get_params_schema(cls) -> List[Dict[str, Any]]:
        """Declare required parameters for this enricher"""
        return [
            {
                "name": "WHOXY_API_KEY",
                "type": "vaultSecret",
                "description": "The Whoxy API key to use for domain history lookups.",
                "required": True,
            },
        ]

    @classmethod
    def name(cls) -> str:
        return "domain_to_history"

    @classmethod
    def category(cls) -> str:
        return "Domain"

    @classmethod
    def key(cls) -> str:
        return "domain"

    async def scan(self, data: List[InputType]) -> List[OutputType]:
        """Find infos related to domains using whoxy api."""
        domains: List[OutputType] = []
        self._extracted_data = []  # Store all extracted data for postprocess
        self._extracted_individuals = []  # Store extracted individuals for testing
        self._extracted_organizations = []  # Store extracted organizations for testing
        api_key = self.get_secret("WHOXY_API_KEY", os.getenv("WHOXY_API_KEY"))

        for i, domain in enumerate(data):
            if i > 0:
                time.sleep(1)
            infos_data = self.__get_infos_from_whoxy(domain.domain, api_key)
            if infos_data and "whois_records" in infos_data:
                # Process each WHOIS record
                for record in infos_data["whois_records"]:
                    if self.__is_valid_whois_record(record):
                        domain_name = record.get("domain_name")
                        if domain_name:
                            domain_obj = Domain(domain=domain_name, root=True)
                            domains.append(domain_obj)

                            # Store extracted data for postprocess
                            extracted_info = {
                                "domain": domain_obj,
                                "original_domain": domain,
                                "whois_record": record,
                                "contacts": {
                                    "registrant": record.get("registrant_contact", {}),
                                    "administrative": record.get(
                                        "administrative_contact", {}
                                    ),
                                    "technical": record.get("technical_contact", {}),
                                    "billing": record.get("billing_contact", {}),
                                },
                            }
                            self._extracted_data.append(extracted_info)

                            # Process contacts and extract individuals during scan
                            self.__process_contacts_during_scan(extracted_info)
        return domains

    def __process_contacts_during_scan(self, extracted_info: Dict[str, Any]):
        """Process contacts and extract individuals and organizations during scan method."""
        domain_name = extracted_info["domain"].domain
        contacts = extracted_info["contacts"]

        for contact_type, contact in contacts.items():
            if contact:
                # Extract individual (if name is not redacted)
                individual = self.__extract_individual_from_contact(
                    contact, contact_type
                )
                if individual:
                    # Store the extracted individual for testing/debugging
                    individual_info = {
                        "individual": individual,
                        "contact_type": contact_type,
                        "domain_name": domain_name,
                        "original_domain": extracted_info["original_domain"].domain,
                        "contact_data": contact,
                    }
                    self._extracted_individuals.append(individual_info)

                # Extract organization (if company name is not redacted)
                organization = self.__extract_organization_from_contact(
                    contact, contact_type
                )
                if organization:
                    # Store the extracted organization for testing/debugging
                    organization_info = {
                        "organization": organization,
                        "contact_type": contact_type,
                        "domain_name": domain_name,
                        "original_domain": extracted_info["original_domain"].domain,
                        "contact_data": contact,
                    }
                    self._extracted_organizations.append(organization_info)


    def __get_infos_from_whoxy(self, domain: str, api_key: str) -> Dict[str, Any]:
        """Get WHOIS history information from Whoxy API or test data."""
        infos: Dict[str, Any] = {}
        whoxy = WhoxyTool()
        try:
            params = {
                "key": api_key,
                "history": domain,
            }
            infos = whoxy.launch(params=params)
        except Exception as e:
            Logger.error(
                self.sketch_id,
                {"message": f"[WHOXY] Whoxy exception for {domain}: {e}"},
            )
        return infos

    def __is_valid_whois_record(self, record: Dict[str, Any]) -> bool:
        """Check if a WHOIS record is valid and contains useful information."""
        domain_name = record.get("domain_name")
        if not domain_name:
            return False

        # A record is valid if it has a domain name - we'll filter contacts individually later
        return True

    def __is_redacted(self, value: str) -> bool:
        """Check if a value is redacted."""
        if not value:
            return True
        return "REDACTED FOR PRIVACY" in value.upper() or "PRIVACY" in value.upper()

    def __extract_individual_from_contact(
        self, contact: Dict[str, Any], contact_type: str
    ) -> Individual:
        """Extract individual information from contact data."""
        full_name = contact.get("full_name", "")

        # Skip if name is redacted - we can't create an individual without a name
        if self.__is_redacted(full_name) or not full_name:
            return None

        # Parse full name into first and last name
        name_parts = full_name.strip().split()
        first_name = name_parts[0] if name_parts else "N/A"
        last_name = " ".join(name_parts[1:]) if len(name_parts) > 1 else "N/A"

        # Extract email and phone
        email_raw = contact.get("email_address", "")
        phone = contact.get("phone_number", "")

        # Handle comma-separated emails
        emails = []
        if email_raw and not self.__is_redacted(email_raw):
            # Split by comma and clean up each email
            email_list = [e.strip() for e in email_raw.split(",")]
            for email in email_list:
                if email and self.__is_valid_email(email):
                    emails.append(email)

        # Skip if phone is redacted
        if self.__is_redacted(phone):
            phone = ""

        # Extract address information
        address = contact.get("mailing_address", "")
        city = contact.get("city_name", "")
        zip_code = contact.get("zip_code", "")
        country = contact.get("country_name", "")

        # Skip if address is redacted
        if self.__is_redacted(address):
            address = ""
        if self.__is_redacted(city):
            city = ""
        if self.__is_redacted(zip_code):
            zip_code = ""
        if self.__is_redacted(country):
            country = ""

        # Create individual object
        individual = Individual(
            first_name=first_name,
            last_name=last_name,
            full_name=full_name,
            email_addresses=emails if emails else None,
            phone_numbers=[phone] if phone else None,
        )

        return individual

    def __is_valid_email(self, email: str) -> bool:
        """Check if email is valid."""
        if not email:
            return False
        # Basic email validation
        pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
        return bool(re.match(pattern, email))

    def __extract_physical_address(self, contact: Dict[str, Any]) -> Location:
        """Extract physical address from contact data."""
        address = contact.get("mailing_address", "")
        city = contact.get("city_name", "")
        zip_code = contact.get("zip_code", "")
        country = contact.get("country_name", "")

        # Skip if any part is redacted
        if any(
            self.__is_redacted(field) for field in [address, city, zip_code, country]
        ):
            return None

        if not all([address, city, zip_code, country]):
            return None

        return Location(address=address, city=city, zip=zip_code, country=country)

    def __extract_organization_from_contact(
        self, contact: Dict[str, Any], contact_type: str
    ) -> Organization:
        """Extract organization information from contact data."""
        company_name = contact.get("company_name", "")

        # Skip if company name is redacted or empty
        if not company_name or self.__is_redacted(company_name):
            return None

        # Create organization object
        organization = Organization(name=company_name)

        return organization

    def postprocess(
        self, results: List[OutputType], original_input: List[InputType]
    ) -> List[OutputType]:
        """Create Neo4j nodes and relationships from extracted data."""
        if not self._graph_service:
            return results

        # Track processed entities to avoid duplicates
        processed_domains: Set[str] = set()
        processed_individuals: Set[str] = set()
        processed_organizations: Set[str] = set()
        processed_emails: Set[str] = set()
        processed_phones: Set[str] = set()
        processed_addresses: Set[str] = set()

        # Process extracted individuals (already filtered and extracted during scan)
        for individual_info in self._extracted_individuals:
            individual = individual_info["individual"]
            contact_type = individual_info["contact_type"]
            domain_name = individual_info["domain_name"]
            original_domain_name = individual_info["original_domain"]

            # Create domain node if not already processed
            if domain_name not in processed_domains:
                processed_domains.add(domain_name)
                domain_obj = Domain(domain=domain_name)
                self.create_node(domain_obj)

                # Create relationship between original domain and found domain
                original_domain_obj = Domain(domain=original_domain_name)
                domain_obj_rel = Domain(domain=domain_name)
                self.create_relationship(
                    original_domain_obj, domain_obj_rel, "HAS_RELATED_DOMAIN"
                )

            # Create individual node if not already processed
            individual_id = (
                f"{individual.first_name}_{individual.last_name}_{individual.full_name}"
            )
            if individual_id not in processed_individuals:
                processed_individuals.add(individual_id)
                self.create_node(individual)

                # Create relationship between individual and domain
                domain_obj_contact = Domain(domain=domain_name)
                self.create_relationship(
                    individual, domain_obj_contact, f"IS_{contact_type.upper()}_CONTACT"
                )

            # Process email addresses
            if individual.email_addresses:
                for email_obj in individual.email_addresses:
                    email_str = email_obj.email
                    if email_str and email_str not in processed_emails:
                        processed_emails.add(email_str)
                        email_node = Email(email=email_str)
                        self.create_node(email_node)
                        self.create_relationship(individual, email_node, "HAS_EMAIL")

            # Process phone numbers
            if individual.phone_numbers:
                for phone_obj in individual.phone_numbers:
                    phone_str = phone_obj.number
                    if phone_str and phone_str not in processed_phones:
                        processed_phones.add(phone_str)
                        phone_node = Phone(number=phone_str)
                        self.create_node(phone_node)
                        self.create_relationship(individual, phone_node, "HAS_PHONE")

            # Process physical address from contact data
            contact_data = individual_info["contact_data"]
            address = self.__extract_physical_address(contact_data)
            if address:
                address_id = (
                    f"{address.address}_{address.city}_{address.zip}_{address.country}"
                )
                if address_id not in processed_addresses:
                    processed_addresses.add(address_id)
                    self.create_node(address)
                    self.create_relationship(individual, address, "LIVES_AT")

            self.log_graph_message(
                f"Processed individual {individual.full_name} ({contact_type}) for domain {domain_name}"
            )

        # Process extracted organizations
        for organization_info in self._extracted_organizations:
            organization = organization_info["organization"]
            contact_type = organization_info["contact_type"]
            domain_name = organization_info["domain_name"]

            # Create domain node if not already processed
            if domain_name not in processed_domains:
                processed_domains.add(domain_name)
                domain_obj = Domain(domain=domain_name)
                self.create_node(domain_obj)

            # Create organization node if not already processed
            if organization.name not in processed_organizations:
                processed_organizations.add(organization.name)
                self.create_node(organization)

                # Create relationship between organization and domain
                domain_obj_org = Domain(domain=domain_name)
                self.create_relationship(
                    organization, domain_obj_org, f"IS_{contact_type.upper()}_CONTACT"
                )

            self.log_graph_message(
                f"Processed organization {organization.name} ({contact_type}) for domain {domain_name}"
            )

        return results


InputType = DomainToHistoryEnricher.InputType
OutputType = DomainToHistoryEnricher.OutputType
