"""
Apify adapter and curated actor catalog for BYO scraper jobs.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable, Dict, List
from urllib.parse import urlparse

from g_gremlin.apify import ApifyClient

from .scraper_provider import (
    SCRAPER_PROVIDER_REGISTRY,
    ScraperProvider,
    ToolCatalogEntry,
    ToolField,
    extract_inputs_from_row,
    filter_output_fields,
    run_test_generic,
)


@dataclass(frozen=True)
class ActorField:
    key: str
    label: str
    required: bool = False
    type: str = "text"
    default: bool = False


@dataclass(frozen=True)
class ActorCatalogEntry:
    id: str
    display_name: str
    description: str
    actor_id: str
    input_schema: List[ActorField]
    output_schema: List[ActorField]
    output_column_prefix: str
    transform: str


# ---------------------------------------------------------------------------
# Transform helpers
# ---------------------------------------------------------------------------


def _get_value(data: dict, *keys: str) -> Any:
    for key in keys:
        value = data.get(key)
        if value not in (None, ""):
            return value
    return ""


def _as_list(value: Any) -> List[Any]:
    if value in (None, ""):
        return []
    if isinstance(value, list):
        return value
    return [value]


def _join_list(value: Any) -> str:
    items = [str(item) for item in _as_list(value) if item not in (None, "")]
    return ", ".join(items)


def _normalize_location(value: Any) -> str:
    if isinstance(value, dict):
        return (
            value.get("linkedinText")
            or value.get("text")
            or value.get("name")
            or str(value)
        )
    return str(value) if value not in (None, "") else ""


def _first_from_list(value: Any) -> str:
    items = _as_list(value)
    for item in items:
        if item not in (None, ""):
            return str(item)
    return ""


def flatten_profile_result(data: dict, inputs: dict) -> dict:
    data = data or {}
    experiences = data.get("experience") or data.get("experiences") or []
    top_experience = experiences[0] if isinstance(experiences, list) and experiences else {}

    email = _get_value(data, "email", "workEmail", "work_email") or _first_from_list(data.get("emails"))
    mobile = _get_value(data, "mobileNumber", "mobile_number", "phone", "phoneNumber") or _first_from_list(data.get("phones"))

    job_title = _get_value(data, "jobTitle", "job_title", "title", "position")
    if not job_title:
        job_title = _get_value(top_experience, "title", "position", "role")

    company_name = _get_value(data, "companyName", "company_name", "company")
    if not company_name:
        company_name = _get_value(top_experience, "company", "companyName", "company_name")

    job_location = _get_value(data, "jobLocation", "job_location", "location")
    job_location = _normalize_location(job_location)

    education = data.get("education") or data.get("educations") or []
    latest_education = ""
    if isinstance(education, list) and education:
        entry = education[0]
        if isinstance(entry, dict):
            latest_education = _get_value(entry, "school", "institution", "name", "degree")
        else:
            latest_education = str(entry)

    return {
        "full_name": _get_value(data, "fullName", "full_name", "name"),
        "email": email,
        "mobile_number": mobile,
        "headline": _get_value(data, "headline", "summary"),
        "job_title": job_title,
        "company_name": company_name,
        "company_industry": _get_value(data, "companyIndustry", "company_industry", "industry"),
        "company_size": _get_value(data, "companySize", "company_size", "employeeRange", "employee_range"),
        "job_location": job_location,
        "connections": _get_value(data, "connections", "connectionCount", "connection_count"),
        "skills_csv": _join_list(data.get("skills")),
        "education_latest": latest_education,
    }


def flatten_jobs_result(data: dict, _inputs: dict) -> dict:
    data = data or {}
    return {
        "title": _get_value(data, "title", "jobTitle", "job_title", "position"),
        "company_name": _get_value(data, "company", "companyName", "company_name", "organization"),
        "location": _normalize_location(_get_value(data, "location", "jobLocation", "job_location")),
        "linkedin_url": _get_value(data, "linkedinUrl", "linkedin_url", "url", "jobUrl", "job_url"),
        "posted_date": _get_value(data, "postedDate", "posted_date", "postedAt", "date"),
        "employment_type": _get_value(data, "employmentType", "employment_type"),
        "workplace_type": _get_value(data, "workplaceType", "workplace_type", "remoteType", "remote_type"),
        "salary": _get_value(data, "salary", "salaryRange", "salary_range"),
        "applicants": _get_value(data, "applicants", "applicantCount", "applicant_count"),
        "description_snippet": _get_value(data, "descriptionSnippet", "snippet", "description", "summary"),
    }


def flatten_google_maps_result(data: dict, _inputs: dict) -> dict:
    data = data or {}
    return {
        "title": _get_value(data, "title", "name"),
        "emails": data.get("emails") or _first_from_list(data.get("email")),
        "phone": _get_value(data, "phone", "phoneNumber", "phone_number"),
        "website": _get_value(data, "website", "websiteUrl", "website_url", "url"),
        "address": _get_value(data, "address", "fullAddress", "full_address"),
        "total_score": _get_value(data, "totalScore", "total_score", "rating", "stars"),
        "reviews_count": _get_value(data, "reviewsCount", "reviews_count", "reviewCount", "review_count"),
        "categories": data.get("categories") or data.get("category") or [],
        "linkedin_url": _get_value(data, "linkedinUrl", "linkedin_url", "linkedin"),
        "opening_hours": data.get("openingHours") or data.get("opening_hours") or data.get("hours") or [],
    }


def flatten_g2_result(data: dict, _inputs: dict) -> dict:
    data = data or {}
    product = data.get("product") or data.get("software") or {}
    if not isinstance(product, dict):
        product = {}
    reviewer = data.get("reviewer") or data.get("author") or data.get("user") or {}
    if not isinstance(reviewer, dict):
        reviewer = {}

    product_name = _get_value(
        data,
        "productName",
        "product_name",
        "product",
        "softwareName",
        "software_name",
    )
    if not product_name and product:
        product_name = _get_value(product, "name", "title")

    reviewer_name = _get_value(
        data,
        "reviewerName",
        "reviewer_name",
        "authorName",
        "author_name",
        "userName",
        "user_name",
    )
    if not reviewer_name and reviewer:
        reviewer_name = _get_value(reviewer, "name", "fullName", "full_name", "username")

    reviewer_title = _get_value(
        data,
        "reviewerTitle",
        "reviewer_title",
        "authorTitle",
        "author_title",
        "jobTitle",
        "job_title",
    )
    if not reviewer_title and reviewer:
        reviewer_title = _get_value(reviewer, "title", "jobTitle", "job_title", "position")

    rating = _get_value(data, "rating", "reviewRating", "review_rating", "score", "stars", "ratingValue")
    if not rating and isinstance(data.get("rating"), dict):
        rating = _get_value(data["rating"], "overall", "value", "score")

    review = data.get("review") or data.get("reviewDetails") or {}
    if not isinstance(review, dict):
        review = {}

    review_text = _get_value(
        data,
        "reviewText",
        "review_text",
        "text",
        "review",
        "comment",
        "content",
        "summary",
        "body",
    )
    if not review_text and review:
        review_text = _get_value(review, "reviewText", "text", "content", "summary", "body")

    pros = _get_value(data, "pros", "prosText", "pros_text", "whatLike", "likes")
    if not pros and review:
        pros = _get_value(review, "pros", "prosText", "pros_text")
    if isinstance(pros, list):
        pros = _join_list(pros)

    cons = _get_value(data, "cons", "consText", "cons_text", "whatDislike", "dislikes")
    if not cons and review:
        cons = _get_value(review, "cons", "consText", "cons_text")
    if isinstance(cons, list):
        cons = _join_list(cons)

    review_date = _get_value(data, "reviewDate", "review_date", "date", "postedAt", "createdAt")
    if not review_date and review:
        review_date = _get_value(review, "date", "postedAt", "createdAt")

    return {
        "product_name": product_name,
        "reviewer_name": reviewer_name,
        "reviewer_title": reviewer_title,
        "rating": rating,
        "review_text": review_text,
        "pros": pros,
        "cons": cons,
        "review_date": review_date,
    }


def flatten_linkedin_posts_result(data: dict, _inputs: dict) -> dict:
    data = data or {}
    post = data.get("post") or data.get("update") or {}
    if not isinstance(post, dict):
        post = {}
    stats = data.get("stats") or data.get("engagement") or data.get("metrics") or {}
    if not isinstance(stats, dict):
        stats = {}

    post_text = _get_value(data, "postText", "post_text", "text", "content", "body", "message")
    if not post_text and post:
        post_text = _get_value(post, "postText", "post_text", "text", "content", "body", "message")

    post_url = _get_value(data, "postUrl", "post_url", "url", "link", "permalink", "shareUrl", "share_url")
    if not post_url and post:
        post_url = _get_value(post, "postUrl", "post_url", "url", "link", "permalink", "shareUrl", "share_url")

    posted_at = _get_value(
        data,
        "postedAt",
        "posted_at",
        "postedDate",
        "postDate",
        "date",
        "timestamp",
        "createdAt",
    )
    if not posted_at and post:
        posted_at = _get_value(post, "postedAt", "posted_at", "date", "timestamp", "createdAt")

    likes = _get_value(data, "likes", "likesCount", "likeCount", "reactions", "reactionsCount")
    if not likes and stats:
        likes = _get_value(stats, "likes", "likesCount", "likeCount", "reactions", "reactionsCount")

    comments = _get_value(data, "comments", "commentCount", "commentsCount", "comments_count")
    if not comments and stats:
        comments = _get_value(stats, "comments", "commentCount", "commentsCount")

    shares = _get_value(data, "shares", "shareCount", "sharesCount", "reposts", "repostsCount")
    if not shares and stats:
        shares = _get_value(stats, "shares", "shareCount", "reposts", "repostsCount")

    media_type = _get_value(data, "mediaType", "media_type", "contentType", "type")
    if not media_type and post:
        media_type = _get_value(post, "mediaType", "media_type", "contentType", "type")
    if not media_type:
        media = data.get("media") or data.get("attachments") or []
        if isinstance(media, list) and media:
            item = media[0]
            if isinstance(item, dict):
                media_type = _get_value(item, "mediaType", "media_type", "contentType", "type")
            else:
                media_type = str(item)
        elif isinstance(media, dict):
            media_type = _get_value(media, "mediaType", "media_type", "contentType", "type")

    return {
        "post_text": post_text,
        "post_url": post_url,
        "posted_at": posted_at,
        "likes": likes,
        "comments": comments,
        "shares": shares,
        "media_type": media_type,
    }


def flatten_google_jobs_result(data: dict, _inputs: dict) -> dict:
    data = data or {}
    job = data.get("job") or data.get("position") or data.get("posting") or {}
    if not isinstance(job, dict):
        job = {}

    title = _get_value(data, "title", "jobTitle", "job_title", "position")
    if not title and job:
        title = _get_value(job, "title", "jobTitle", "job_title", "position")

    company_name = _get_value(data, "company", "companyName", "company_name", "employer", "organization")
    if not company_name and job:
        company_name = _get_value(job, "company", "companyName", "company_name", "employer", "organization")

    location = _normalize_location(_get_value(data, "location", "jobLocation", "job_location"))
    if not location and job:
        location = _normalize_location(_get_value(job, "location", "jobLocation", "job_location"))

    description_snippet = _get_value(
        data,
        "descriptionSnippet",
        "description_snippet",
        "snippet",
        "description",
        "summary",
        "jobDescription",
        "job_description",
    )
    if not description_snippet and job:
        description_snippet = _get_value(job, "descriptionSnippet", "description", "summary", "snippet")

    posted_date = _get_value(data, "postedDate", "posted_date", "datePosted", "date", "postedAt", "createdAt")
    if not posted_date and job:
        posted_date = _get_value(job, "postedDate", "datePosted", "postedAt", "createdAt")

    apply_url = _get_value(
        data,
        "applyUrl",
        "apply_url",
        "applyLink",
        "apply_link",
        "jobUrl",
        "job_url",
        "url",
        "link",
    )
    if not apply_url and job:
        apply_url = _get_value(job, "applyUrl", "apply_link", "url", "link", "jobUrl", "job_url")
    if not apply_url and isinstance(data.get("applyOptions"), list):
        option = data["applyOptions"][0] if data["applyOptions"] else {}
        if isinstance(option, dict):
            apply_url = _get_value(option, "url", "link", "applyUrl", "apply_url")

    source = _get_value(data, "source", "provider", "via", "publisher", "site")
    if not source and job:
        source = _get_value(job, "source", "provider", "via", "publisher", "site")

    return {
        "title": title,
        "company_name": company_name,
        "location": location,
        "description_snippet": description_snippet,
        "posted_date": posted_date,
        "apply_url": apply_url,
        "source": source,
    }


ACTOR_TRANSFORMS: Dict[str, Callable[[dict, dict], dict]] = {
    "flatten_profile_result": flatten_profile_result,
    "flatten_jobs_result": flatten_jobs_result,
    "flatten_google_maps_result": flatten_google_maps_result,
    "flatten_g2_result": flatten_g2_result,
    "flatten_linkedin_posts_result": flatten_linkedin_posts_result,
    "flatten_google_jobs_result": flatten_google_jobs_result,
}


def _extract_g2_product(url: str) -> tuple[str, str]:
    try:
        parsed = urlparse(url)
    except Exception:
        return "", ""
    parts = [part for part in parsed.path.split("/") if part]
    if "products" in parts:
        index = parts.index("products")
        if index + 1 < len(parts):
            slug = parts[index + 1]
            if slug:
                return slug, slug.replace("-", " ")
    return "", ""


def _build_profile_input(values: dict) -> dict:
    url = (values.get("linkedin_url") or values.get("profile_url") or "").strip()
    if not url:
        return {}
    return {
        "linkedin_url": url,
        "profile_url": url,
        "profileUrls": [url],
        "profile_urls": [url],
        "startUrls": [{"url": url}],
        "start_urls": [{"url": url}],
        "urls": [url],
        "maxItems": 1,
    }


def _build_jobs_input(values: dict) -> dict:
    job_title = (values.get("job_title") or "").strip()
    company = (values.get("company") or "").strip()
    location = (values.get("location") or "").strip()
    parts = [part for part in [job_title, company] if part]
    query = " ".join(parts).strip()

    payload = {
        "job_title": job_title,
        "company": company,
        "location": location,
        "maxResults": 1,
        "maxItems": 1,
    }
    if query:
        payload.update({
            "searchQuery": query,
            "search_query": query,
            "keywords": query,
            "query": query,
        })
    return {k: v for k, v in payload.items() if v not in (None, "")}


def _build_google_maps_input(values: dict) -> dict:
    search_query = (values.get("search_query") or "").strip()
    location = (values.get("location") or "").strip()
    max_results = values.get("max_results")
    try:
        max_results = int(max_results) if max_results not in (None, "") else None
    except (TypeError, ValueError):
        max_results = None

    combined = " ".join([part for part in [search_query, location] if part]).strip()

    payload = {
        "search_query": search_query,
        "location": location,
        "max_results": max_results,
        "searchQuery": combined or search_query,
        "searchString": combined or search_query,
    }
    if max_results:
        payload.update({
            "maxResults": max_results,
            "maxItems": max_results,
        })
    return {k: v for k, v in payload.items() if v not in (None, "")}


def _build_g2_input(values: dict) -> dict:
    url = (
        values.get("g2_url")
        or values.get("product_url")
        or values.get("url")
        or ""
    ).strip()
    if not url:
        return {}

    slug, name = _extract_g2_product(url)
    payload: Dict[str, Any] = {
        "url": url,
        "productUrl": url,
        "product_url": url,
        "startUrl": url,
        "startUrls": [url],
        "start_urls": [url],
        "limit": 1,
        "maxItems": 1,
    }
    if slug:
        payload.update({
            "product": slug,
            "productSlug": slug,
            "product_slug": slug,
        })
    if name:
        payload.update({
            "productName": name,
            "product_name": name,
        })
    return {k: v for k, v in payload.items() if v not in (None, "")}


def _build_linkedin_posts_input(values: dict) -> dict:
    company = (
        values.get("linkedin_company_url")
        or values.get("company_url")
        or values.get("company_name")
        or values.get("company")
        or values.get("linkedin_url")
        or values.get("url")
        or ""
    ).strip()
    if not company:
        return {}

    payload: Dict[str, Any] = {
        "company_name": company,
        "companyName": company,
        "company": company,
        "companyUrl": company,
        "company_url": company,
        "limit": 1,
        "page_number": 1,
        "sort": "recent",
    }
    if company.startswith("http"):
        payload.update({
            "startUrls": [{"url": company}],
            "start_urls": [{"url": company}],
            "urls": [company],
        })
    return {k: v for k, v in payload.items() if v not in (None, "")}


def _build_google_jobs_input(values: dict) -> dict:
    job_title = (values.get("job_title") or "").strip()
    company = (values.get("company") or "").strip()
    location = (values.get("location") or "").strip()
    if not job_title:
        return {}

    query_parts = [part for part in [job_title, company, location] if part]
    query = " ".join(query_parts).strip()

    payload: Dict[str, Any] = {
        "query": query,
        "query_input": query,
        "keyword": query,
        "search_text": query,
        "searchQuery": query,
        "job_title": job_title,
        "company": company,
        "companyName": company,
        "company_name": company,
        "location": location,
        "num_results": 10,
        "max_pagination": 1,
        "result_limit": 1,
        "maxItems": 1,
    }
    return {k: v for k, v in payload.items() if v not in (None, "")}


ACTOR_INPUT_BUILDERS: Dict[str, Callable[[dict], dict]] = {
    "profile_enricher": _build_profile_input,
    "jobs_finder": _build_jobs_input,
    "google_maps_leads": _build_google_maps_input,
    "g2_reviews": _build_g2_input,
    "linkedin_posts": _build_linkedin_posts_input,
    "google_jobs": _build_google_jobs_input,
}


ACTOR_CATALOG: Dict[str, ActorCatalogEntry] = {
    "profile_enricher": ActorCatalogEntry(
        id="profile_enricher",
        display_name="Profile Enrichment",
        description="Deep LinkedIn profile data with verified email & phone",
        actor_id="dev_fusion/Linkedin-Profile-Scraper",
        input_schema=[
            ActorField(key="linkedin_url", label="LinkedIn URL", required=True, type="url"),
        ],
        output_schema=[
            ActorField(key="full_name", label="Full Name", default=True),
            ActorField(key="email", label="Email", default=True),
            ActorField(key="mobile_number", label="Phone", default=True),
            ActorField(key="headline", label="Headline", default=True),
            ActorField(key="job_title", label="Current Title", default=True),
            ActorField(key="company_name", label="Current Company", default=True),
            ActorField(key="company_industry", label="Industry", default=False),
            ActorField(key="company_size", label="Company Size", default=False),
            ActorField(key="job_location", label="Location", default=False),
            ActorField(key="connections", label="Connections", default=False),
            ActorField(key="skills_csv", label="Skills", default=False),
            ActorField(key="education_latest", label="Education", default=False),
        ],
        output_column_prefix="Profile_",
        transform="flatten_profile_result",
    ),
    "jobs_finder": ActorCatalogEntry(
        id="jobs_finder",
        display_name="Jobs Finder",
        description="Find job postings for hiring/expansion signals",
        actor_id="harvestapi/linkedin-job-search",
        input_schema=[
            ActorField(key="job_title", label="Job Title", required=True),
            ActorField(key="location", label="Location", required=False),
            ActorField(key="company", label="Company", required=False),
        ],
        output_schema=[
            ActorField(key="title", label="Job Title", default=True),
            ActorField(key="company_name", label="Company", default=True),
            ActorField(key="location", label="Location", default=True),
            ActorField(key="linkedin_url", label="Job URL", default=True),
            ActorField(key="posted_date", label="Posted Date", default=True),
            ActorField(key="employment_type", label="Employment Type", default=False),
            ActorField(key="workplace_type", label="Workplace Type", default=False),
            ActorField(key="salary", label="Salary", default=False),
            ActorField(key="applicants", label="Applicants", default=False),
            ActorField(key="description_snippet", label="Description", default=False),
        ],
        output_column_prefix="Jobs_",
        transform="flatten_jobs_result",
    ),
    "google_maps_leads": ActorCatalogEntry(
        id="google_maps_leads",
        display_name="Google Maps Leads",
        description="Find local businesses with emails & contact info",
        actor_id="lukaskrivka/google-maps-with-contact-details",
        input_schema=[
            ActorField(key="search_query", label="Search Query", required=True),
            ActorField(key="location", label="Location", required=True),
            ActorField(key="max_results", label="Max Results", required=False, type="number"),
        ],
        output_schema=[
            ActorField(key="title", label="Business Name", default=True),
            ActorField(key="emails", label="Emails", default=True),
            ActorField(key="phone", label="Phone", default=True),
            ActorField(key="website", label="Website", default=True),
            ActorField(key="address", label="Address", default=True),
            ActorField(key="total_score", label="Rating", default=False),
            ActorField(key="reviews_count", label="Reviews", default=False),
            ActorField(key="categories", label="Categories", default=False),
            ActorField(key="linkedin_url", label="LinkedIn", default=False),
            ActorField(key="opening_hours", label="Hours", default=False),
        ],
        output_column_prefix="Maps_",
        transform="flatten_google_maps_result",
    ),
    "g2_reviews": ActorCatalogEntry(
        id="g2_reviews",
        display_name="G2 Product Reviews",
        description="Scrape G2 product reviews for competitive intelligence",
        actor_id="zen-studio/g2-reviews-scraper",
        input_schema=[
            ActorField(key="g2_url", label="G2 Product URL", required=True, type="url"),
        ],
        output_schema=[
            ActorField(key="product_name", label="Product", default=True),
            ActorField(key="reviewer_name", label="Reviewer", default=True),
            ActorField(key="reviewer_title", label="Reviewer Title", default=False),
            ActorField(key="rating", label="Rating", default=True),
            ActorField(key="review_text", label="Review Text", default=True),
            ActorField(key="pros", label="Pros", default=False),
            ActorField(key="cons", label="Cons", default=False),
            ActorField(key="review_date", label="Review Date", default=True),
        ],
        output_column_prefix="G2_",
        transform="flatten_g2_result",
    ),
    "linkedin_posts": ActorCatalogEntry(
        id="linkedin_posts",
        display_name="LinkedIn Company Posts",
        description="Scrape recent posts from a company's LinkedIn page for intent signals",
        actor_id="apimaestro/linkedin-company-posts",
        input_schema=[
            ActorField(
                key="linkedin_company_url",
                label="LinkedIn Company URL",
                required=True,
                type="url",
            ),
        ],
        output_schema=[
            ActorField(key="post_text", label="Post Text", default=True),
            ActorField(key="post_url", label="Post URL", default=True),
            ActorField(key="posted_at", label="Posted At", default=True),
            ActorField(key="likes", label="Likes", default=True),
            ActorField(key="comments", label="Comments", default=False),
            ActorField(key="shares", label="Shares", default=False),
            ActorField(key="media_type", label="Media Type", default=False),
        ],
        output_column_prefix="Posts_",
        transform="flatten_linkedin_posts_result",
    ),
    "google_jobs": ActorCatalogEntry(
        id="google_jobs",
        display_name="Google Jobs Aggregator",
        description="Scrape job postings from Google Jobs (aggregates Indeed, Glassdoor, etc.)",
        actor_id="johnvc/Google-Jobs-Scraper",
        input_schema=[
            ActorField(key="job_title", label="Job Title", required=True),
            ActorField(key="location", label="Location", required=False),
            ActorField(key="company", label="Company", required=False),
        ],
        output_schema=[
            ActorField(key="title", label="Job Title", default=True),
            ActorField(key="company_name", label="Company", default=True),
            ActorField(key="location", label="Location", default=True),
            ActorField(key="description_snippet", label="Description", default=False),
            ActorField(key="posted_date", label="Posted Date", default=True),
            ActorField(key="apply_url", label="Apply URL", default=True),
            ActorField(key="source", label="Source", default=False),
        ],
        output_column_prefix="GJobs_",
        transform="flatten_google_jobs_result",
    ),
}


class ApifyScraperService:
    """Adapter to run curated Apify actors for scraper jobs."""

    def __init__(self, api_token: str):
        self.api_token = api_token
        self.client = ApifyClient(api_token=api_token)

    @staticmethod
    def list_actors() -> List[dict]:
        return [
            {
                "id": entry.id,
                "display_name": entry.display_name,
                "description": entry.description,
                "actor_id": entry.actor_id,
                "input_schema": {
                    "fields": [field.__dict__ for field in entry.input_schema],
                },
                "output_schema": {
                    "fields": [
                        {
                            "key": field.key,
                            "label": field.label,
                            "default": field.default,
                        }
                        for field in entry.output_schema
                    ],
                },
                "output_column_prefix": entry.output_column_prefix,
                "transform": entry.transform,
            }
            for entry in ACTOR_CATALOG.values()
        ]

    @staticmethod
    def get_actor(actor_type: str) -> ActorCatalogEntry:
        actor = ACTOR_CATALOG.get(actor_type)
        if not actor:
            raise ValueError(f"Unknown actor type: {actor_type}")
        return actor

    @staticmethod
    def required_keys(actor: ActorCatalogEntry) -> List[str]:
        return [field.key for field in actor.input_schema if field.required]

    @staticmethod
    def _get_row_index(row: dict, fallback: int) -> int:
        value = row.get("__fm_row_index__")
        try:
            return int(value)
        except (TypeError, ValueError):
            return fallback

    def validate_rows(
        self,
        rows: List[dict],
        input_mapping: Dict[str, str],
        actor_type: str,
    ) -> Dict[str, int]:
        actor = self.get_actor(actor_type)
        required_keys = [field.key for field in actor.input_schema if field.required]
        valid = 0
        skipped = 0
        for row in rows:
            inputs = self._extract_inputs(row, input_mapping, actor)
            if all(inputs.get(key) for key in required_keys):
                valid += 1
            else:
                skipped += 1
        return {
            "rows_with_valid_inputs": valid,
            "rows_skipped": skipped,
            "estimated_credits": valid,
        }

    def extract_inputs(self, row: dict, input_mapping: Dict[str, str], actor: ActorCatalogEntry) -> dict:
        return self._extract_inputs(row, input_mapping, actor)

    def run_actor(self, inputs: dict, actor: ActorCatalogEntry, output_fields: List[str]) -> dict:
        result = self._run_actor_for_inputs(inputs, actor)
        return self._filter_output_fields(result, output_fields)

    def run_test(
        self,
        rows: List[dict],
        input_mapping: Dict[str, str],
        output_fields: List[str],
        actor_type: str,
        max_rows: int = 5,
    ) -> List[dict]:
        actor = self.get_actor(actor_type)
        results = []
        for idx, row in enumerate(rows[:max_rows]):
            row_index = self._get_row_index(row, idx)
            inputs = self._extract_inputs(row, input_mapping, actor)
            required_keys = [field.key for field in actor.input_schema if field.required]
            if not all(inputs.get(key) for key in required_keys):
                results.append(
                    {
                        "row_index": row_index,
                        "status": "skipped",
                        "error_message": "Missing required input",
                    }
                )
                continue
            try:
                result = self._run_actor_for_inputs(inputs, actor)
            except Exception as exc:
                results.append(
                    {
                        "row_index": row_index,
                        "status": "error",
                        "error_message": str(exc),
                    }
                )
                continue

            filtered = self._filter_output_fields(result, output_fields)
            filtered["row_index"] = row_index
            filtered["status"] = "scraped"
            results.append(filtered)

        return results

    def _extract_inputs(self, row: dict, input_mapping: Dict[str, str], actor: ActorCatalogEntry) -> dict:
        inputs: Dict[str, Any] = {}
        for field in actor.input_schema:
            col = input_mapping.get(field.key)
            if not col:
                continue
            value = row.get(col)
            if value is None:
                continue
            text = str(value).strip()
            if text == "":
                continue
            inputs[field.key] = text
        return inputs

    def _run_actor_for_inputs(self, inputs: dict, actor: ActorCatalogEntry) -> dict:
        builder = ACTOR_INPUT_BUILDERS.get(actor.id)
        if not builder:
            raise ValueError(f"No input builder for actor {actor.id}")
        run_input = builder(inputs)
        if not run_input:
            raise ValueError("Missing required input")

        run, items = self.client._run_actor_sync(
            actor_id=actor.actor_id,
            run_input=run_input,
            timeout_secs=120,
            wait_secs=300,
        )
        data = items[0] if items else {}
        transform = ACTOR_TRANSFORMS.get(actor.transform)
        if not transform:
            raise ValueError(f"Missing transform {actor.transform}")
        return transform(data, inputs)

    @staticmethod
    def _filter_output_fields(result: dict, output_fields: List[str]) -> dict:
        if not output_fields:
            return result
        filtered = {key: result.get(key, "") for key in output_fields}
        return filtered


class ApifyScraperProvider(ScraperProvider):
    provider_name = "apify"

    def __init__(self, api_token: str | None = None) -> None:
        self.api_token = api_token or ""
        self._service = ApifyScraperService(api_token=self.api_token) if self.api_token else None

    @staticmethod
    def _actor_to_tool(entry: ActorCatalogEntry) -> ToolCatalogEntry:
        return ToolCatalogEntry(
            id=entry.id,
            provider="apify",
            display_name=entry.display_name,
            description=entry.description,
            provider_tool_id=entry.actor_id,
            input_schema=[
                ToolField(
                    key=field.key,
                    label=field.label,
                    required=field.required,
                    type=field.type,
                    default=field.default,
                )
                for field in entry.input_schema
            ],
            output_schema=[
                ToolField(
                    key=field.key,
                    label=field.label,
                    required=field.required,
                    type=field.type,
                    default=field.default,
                )
                for field in entry.output_schema
            ],
            output_column_prefix=entry.output_column_prefix,
            transform=entry.transform,
        )

    @staticmethod
    def _require_service(api_token: str, service: ApifyScraperService | None) -> ApifyScraperService:
        if service is not None:
            return service
        if not api_token:
            raise ValueError("Apify not connected")
        return ApifyScraperService(api_token=api_token)

    def list_tools(self) -> List[ToolCatalogEntry]:
        return [self._actor_to_tool(entry) for entry in ACTOR_CATALOG.values()]

    def get_tool(self, tool_id: str) -> ToolCatalogEntry:
        actor = ApifyScraperService.get_actor(tool_id)
        return self._actor_to_tool(actor)

    def validate_rows(
        self,
        rows: List[Dict[str, Any]],
        input_mapping: Dict[str, str],
        tool_id: str,
    ) -> Dict[str, int]:
        tool = self.get_tool(tool_id)
        valid = 0
        skipped = 0
        for row in rows:
            inputs = self.extract_inputs(row, input_mapping, tool)
            if all(inputs.get(key) for key in tool.required_keys()):
                valid += 1
            else:
                skipped += 1
        return {
            "rows_with_valid_inputs": valid,
            "rows_skipped": skipped,
            "estimated_credits": valid,
        }

    def extract_inputs(
        self,
        row: Dict[str, Any],
        input_mapping: Dict[str, str],
        tool: ToolCatalogEntry,
    ) -> Dict[str, Any]:
        return extract_inputs_from_row(row, input_mapping, tool.input_schema)

    def run_single(
        self,
        inputs: Dict[str, Any],
        tool: ToolCatalogEntry,
        output_fields: List[str],
    ) -> Dict[str, Any]:
        service = self._require_service(self.api_token, self._service)
        actor = ApifyScraperService.get_actor(tool.id)
        result = service._run_actor_for_inputs(inputs, actor)
        return filter_output_fields(result, output_fields)

    def run_test(
        self,
        rows: List[Dict[str, Any]],
        input_mapping: Dict[str, str],
        output_fields: List[str],
        tool_id: str,
        max_rows: int = 5,
    ) -> List[Dict[str, Any]]:
        return run_test_generic(
            self,
            rows=rows,
            input_mapping=input_mapping,
            output_fields=output_fields,
            tool_id=tool_id,
            max_rows=max_rows,
        )


SCRAPER_PROVIDER_REGISTRY.register("apify", ApifyScraperProvider)
