"""
SOQL Builder API - Constrained and safe for free tier
"""

from fastapi import APIRouter, Depends, HTTPException, Request, Query
from typing import Dict, Any, List, Optional
from pydantic import BaseModel
import json
import re

from .deps import require_api_key
from ...services.soql_query_policy import (
    canonical_safe_object_name,
    safe_field_names_for_object,
)

router = APIRouter(prefix="/salesforce/query-builder", tags=["SOQL Builder"])


class SOQLValidationRequest(BaseModel):
    soql: str
    validate_only: bool = True


class SOQLValidationResponse(BaseModel):
    valid: bool
    warnings: List[str] = []
    estimated_rows: Optional[int] = None
    api_calls_remaining: int
    projected_api_calls: int
    will_hit_limit: bool = False


@router.get("/objects")
async def list_queryable_objects(api_ctx=Depends(require_api_key)):
    """
    List queryable Salesforce objects (filtered for safety).
    Only returns: queryable=true AND supportsFeed=false
    """
    from ...services.salesforce_gateway import SalesforceGateway
    from ...db import get_session

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    async for db in get_session():
        try:
            gateway = await SalesforceGateway.for_tenant(str(account_id), db)

            # Get all objects
            all_objects = await gateway.describe_global()

            # Filter to safe, queryable objects
            safe_objects = [
                {
                    "name": obj["name"],
                    "label": obj["label"],
                    "labelPlural": obj.get("labelPlural", obj["label"]),
                    "keyPrefix": obj.get("keyPrefix"),
                }
                for obj in all_objects.get("sobjects", [])
                if obj.get("queryable")
                and not obj.get("supportsFeed", False)  # Exclude feed objects
                and not obj["name"].endswith("__Feed")
                and not obj["name"].endswith("__History")
                and not obj["name"].endswith("__Share")
            ]

            # Sort by label
            safe_objects.sort(key=lambda x: x["label"])

            return {"ok": True, "data": {"objects": safe_objects}}
        except Exception as e:
            raise HTTPException(500, f"Failed to fetch objects: {e}")


@router.api_route("/fields", methods=["GET", "POST"])
async def get_object_fields(
    request: Request,
    object_name: Optional[str] = Query(None, alias="object"),
    api_ctx=Depends(require_api_key),
):
    """
    Get fields for a specific object with metadata for safe queries.
    Returns: sortable, filterable, aggregatable flags
    """
    from ...services.salesforce_gateway import SalesforceGateway
    from ...db import get_session

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    if not object_name and request.method == "POST":
        if request.method == "POST":
            try:
                body = await request.json()
            except Exception:
                body = {}
            object_name = body.get("object") or body.get("object_name")
    if not object_name:
        raise HTTPException(400, "Missing object parameter")

    async for db in get_session():
        try:
            gateway = await SalesforceGateway.for_tenant(str(account_id), db)

            # Get object describe
            describe = await gateway.describe_object(object_name)
            resolved_object_name = describe.get("name") or object_name
            allowlist_object = canonical_safe_object_name(
                resolved_object_name
            ) or canonical_safe_object_name(object_name)
            allowlisted_fields = (
                safe_field_names_for_object(allowlist_object)
                if allowlist_object
                else None
            )

            # Process fields with safety metadata and query-execution policy.
            fields = []
            hidden_by_policy = 0
            hidden_by_schema = 0
            for field in describe.get("fields", []):
                field_name = str(field.get("name") or "").strip()
                if not field_name:
                    continue
                field_type = str(field.get("type") or "").strip()
                field_type_lower = field_type.lower()
                if field_type_lower in {"address", "location"} or bool(
                    field.get("deprecatedAndHidden")
                ):
                    hidden_by_schema += 1
                    continue
                if not field.get("queryable", True) or not field.get(
                    "accessible", True
                ):
                    hidden_by_schema += 1
                    continue
                if (
                    allowlisted_fields is not None
                    and field_name.lower() not in allowlisted_fields
                ):
                    hidden_by_policy += 1
                    continue

                field_info = {
                    "name": field_name,
                    "label": field.get("label") or field_name,
                    "type": field_type,
                    "sortable": field.get("sortable", False),
                    "filterable": field.get("filterable", False),
                    "groupable": field.get("groupable", False),
                    "aggregatable": field.get("aggregatable", False),
                    "nillable": field.get("nillable", True),
                    "relationshipName": field.get("relationshipName"),
                    "referenceTo": field.get("referenceTo", []),
                    "length": field.get("length"),
                    "precision": field.get("precision"),
                    "scale": field.get("scale"),
                    "calculated": field.get("calculated", False),
                    "restrictedPicklist": field.get("restrictedPicklist", False),
                    "dependentPicklist": field.get("dependentPicklist", False),
                    "controllerName": field.get("controllerName"),
                    "helpText": field.get("inlineHelpText") or field.get("description"),
                }

                picklist_values = field.get("picklistValues") or []
                if picklist_values:
                    trimmed = []
                    for entry in picklist_values[:200]:
                        value = entry.get("value")
                        if value in (None, ""):
                            continue
                        trimmed.append(
                            {
                                "value": str(value),
                                "label": str(entry.get("label") or value),
                                "default": bool(entry.get("defaultValue")),
                            }
                        )
                    field_info["picklistValues"] = trimmed

                # Mark relationship fields
                if field.get("relationshipName"):
                    field_info["isRelationship"] = True
                    field_info["warning"] = (
                        "Relationship field - counts as parent lookup (use Join-Lite for complex joins)"
                    )

                fields.append(field_info)

            # Sort: standard fields first, then custom
            fields.sort(key=lambda x: (x["name"].endswith("__c"), x["label"]))

            return {
                "ok": True,
                "data": {
                    "object": resolved_object_name,
                    "fields": fields,
                    "totalFields": len(fields),
                    "hiddenByPolicy": hidden_by_policy,
                    "hiddenBySchema": hidden_by_schema,
                    "allowlistObject": allowlist_object,
                },
            }
        except Exception as e:
            import logging

            logger = logging.getLogger(__name__)
            logger.error(
                f"SOQL Builder fields error - account_id={account_id}, "
                f"object={object_name}, error={str(e)}",
                exc_info=True,
            )
            raise HTTPException(500, f"Failed to fetch fields: {e}")


@router.api_route("/field-values", methods=["GET", "POST"])
async def get_field_values(
    request: Request,
    object_name: Optional[str] = Query(None, alias="object"),
    field: Optional[str] = Query(None),
    limit: int = Query(10),
    api_ctx=Depends(require_api_key),
):
    """
    Return a de-duplicated sample of field values to help users build filters.
    """
    from ...services.salesforce_gateway import SalesforceGateway
    from ...db import get_session

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    if request.method == "POST":
        try:
            body = await request.json()
        except Exception:
            body = {}
        object_name = object_name or body.get("object") or body.get("object_name")
        field = field or body.get("field")
        if "limit" in body and body["limit"] is not None:
            try:
                limit = int(body["limit"])
            except (ValueError, TypeError):
                raise HTTPException(400, "Invalid limit parameter")

    if not object_name or not re.fullmatch(r"[A-Za-z0-9_]+", str(object_name)):
        raise HTTPException(400, "Invalid object name")
    if not field or not re.fullmatch(r"[A-Za-z0-9_.]+", str(field)):
        raise HTTPException(400, "Invalid field name")

    try:
        limit = int(limit or 10)
    except (ValueError, TypeError):
        raise HTTPException(400, "Invalid limit parameter")

    limit = max(1, min(limit, 25))

    async for db in get_session():
        try:
            gateway = await SalesforceGateway.for_tenant(str(account_id), db)
            describe = await gateway.describe_object(object_name)
            field_map = {f["name"]: f for f in describe.get("fields", [])}

            path_parts = field.split(".")
            is_relationship = len(path_parts) > 1
            leaf_field = path_parts[-1]

            related_object = None
            base_field_meta = None
            related_describe = None

            if is_relationship:
                relationship_name = path_parts[0]
                rel_field_meta = next(
                    (
                        f
                        for f in describe.get("fields", [])
                        if f.get("relationshipName") == relationship_name
                    ),
                    None,
                )
                if not rel_field_meta:
                    raise HTTPException(
                        400,
                        f"Relationship {relationship_name} not found on {object_name}",
                    )
                reference_to = rel_field_meta.get("referenceTo") or []
                if not reference_to:
                    raise HTTPException(
                        400, f"No reference object for relationship {relationship_name}"
                    )
                related_object = reference_to[0]
                related_describe = await gateway.describe_object(related_object)
                related_map = {f["name"]: f for f in related_describe.get("fields", [])}
                base_field_meta = related_map.get(leaf_field)
                if not base_field_meta:
                    raise HTTPException(
                        400, f"Field {leaf_field} not found on {related_object}"
                    )
            else:
                base_field_meta = field_map.get(field)
                if not base_field_meta:
                    raise HTTPException(
                        400, f"Field {field} not found on {object_name}"
                    )

            display_field_path = field
            reference_relationship_name: Optional[str] = None
            reference_targets = (
                base_field_meta.get("referenceTo", []) if base_field_meta else []
            )
            reference_multi_target = len(reference_targets) > 1

            if (
                not is_relationship
                and base_field_meta
                and base_field_meta.get("type") == "reference"
                and not reference_multi_target
            ):
                reference_relationship_name = base_field_meta.get("relationshipName")
                if reference_relationship_name:
                    display_field_path = f"{reference_relationship_name}.Name"
                    if not related_object and reference_targets:
                        related_object = reference_targets[0]
                else:
                    display_field_path = field
                if related_object and not related_describe:
                    related_describe = await gateway.describe_object(related_object)
            if is_relationship:
                filter_field = rel_field_meta.get("name") if rel_field_meta else None
            else:
                filter_field = (
                    field if base_field_meta.get("filterable", False) else None
                )

            def _build_allowlist() -> Dict[str, Any]:
                objects = {object_name}
                fields_map: Dict[str, set[str]] = {}
                base_key = path_parts[0] if is_relationship else leaf_field
                fields_map.setdefault(object_name.lower(), set()).add(base_key)
                if filter_field:
                    fields_map.setdefault(object_name.lower(), set()).add(filter_field)
                if is_relationship and related_object:
                    objects.add(related_object)
                    fields_map.setdefault(related_object.lower(), set()).add(leaf_field)
                if (
                    not is_relationship
                    and reference_relationship_name
                    and not reference_multi_target
                ):
                    fields_map.setdefault(object_name.lower(), set()).add(
                        reference_relationship_name
                    )
                if (
                    related_object
                    and display_field_path != field
                    and not reference_multi_target
                ):
                    objects.add(related_object)
                    fields_map.setdefault(related_object.lower(), set()).add("Name")
                return {
                    "objects": list(objects),
                    "fields": {
                        obj: sorted(list(vals)) for obj, vals in fields_map.items()
                    },
                }

            plan_allowlist = _build_allowlist()

            sample_limit = limit * 5

            async def _run_sample(where_clause: str) -> List[Dict[str, Any]]:
                clause = f" WHERE {where_clause}" if where_clause else ""
                select_fields: List[str] = []
                for candidate in (field, display_field_path):
                    if candidate and candidate not in select_fields:
                        select_fields.append(candidate)
                soql = f"SELECT {', '.join(select_fields)} FROM {object_name}{clause} LIMIT {sample_limit}"
                result = await gateway.soql(
                    soql,
                    plan_allowlist=plan_allowlist,
                    fls_guard=False,
                )
                return result.get("records", []) or []

            records = await _run_sample(
                f"{filter_field} != NULL" if filter_field else ""
            )
            if not records and filter_field:
                records = await _run_sample("")

            async def _run_grouped(where_clause: str) -> List[Dict[str, Any]]:
                clause = f" WHERE {where_clause}" if where_clause else ""
                soql = f"SELECT {field} FROM {object_name}{clause} GROUP BY {field} LIMIT {sample_limit}"
                result = await gateway.soql(
                    soql,
                    plan_allowlist=plan_allowlist,
                    fls_guard=False,
                )
                return result.get("records", []) or []

            can_group = bool(base_field_meta.get("groupable", False))
            # Grouping on relationship display fields is unsupported, so disable when lookups
            if display_field_path != field:
                can_group = False
            if is_relationship and base_field_meta is not None:
                can_group = can_group or bool(base_field_meta.get("groupable", False))

            if not records and can_group:
                try:
                    records = await _run_grouped(
                        f"{filter_field} != NULL" if filter_field else ""
                    )
                    if not records and filter_field:
                        records = await _run_grouped("")
                except Exception:
                    records = records or []

            def _extract_value(record: Dict[str, Any], path: str) -> Optional[Any]:
                current: Any = record
                for part in path.split("."):
                    if not isinstance(current, dict):
                        return None
                    current = current.get(part)
                    if current is None:
                        return None
                if isinstance(current, dict):
                    for key in ("Name", "name", "FullName", "Label", "value", "Id"):
                        if key in current and current[key] not in (None, ""):
                            return current[key]
                    return json.dumps(current, ensure_ascii=False)
                return current

            seen: set[str] = set()
            values: List[Dict[str, Any]] = []
            display_index: Dict[str, List[int]] = {}

            for record in records:
                extracted = _extract_value(record, field)
                if extracted in (None, ""):
                    continue
                raw_string = extracted if isinstance(extracted, str) else str(extracted)
                if not raw_string:
                    continue
                if raw_string in seen:
                    continue
                seen.add(raw_string)

                display_value = raw_string
                if display_field_path != field:
                    display_candidate = _extract_value(record, display_field_path)
                    if display_candidate not in (None, ""):
                        display_value = (
                            display_candidate
                            if isinstance(display_candidate, str)
                            else str(display_candidate)
                        )
                else:
                    if isinstance(extracted, str):
                        display_value = extracted
                    else:
                        display_value = str(extracted)

                raw_for_insert = (
                    extracted if display_field_path == field else raw_string
                )

                display_index.setdefault(display_value, []).append(len(values))

                entry: Dict[str, Any] = {"value": display_value, "raw": raw_for_insert}
                if display_field_path != field:
                    entry["id"] = raw_string

                values.append(entry)
                if len(values) >= limit:
                    break

            for label, idxs in display_index.items():
                if len(idxs) > 1:
                    for idx in idxs:
                        entry = values[idx]
                        identifier = entry.get("id") or entry.get("raw")
                        if not identifier:
                            continue
                        identifier_str = str(identifier)
                        short_id = (
                            f"{identifier_str[:6]}…{identifier_str[-4:]}"
                            if len(identifier_str) > 12
                            else identifier_str
                        )
                        entry["value"] = f"{label} ({short_id})"

            return {
                "ok": True,
                "data": {"values": values, "field": field, "object": object_name},
            }
        except HTTPException:
            raise
        except Exception as exc:  # noqa: BLE001
            raise HTTPException(500, f"Failed to fetch field values: {exc}") from exc


@router.post("/validate")
async def validate_soql(
    request: SOQLValidationRequest, api_ctx=Depends(require_api_key)
):
    """
    Validate SOQL and return warnings + estimates.
    Guards against expensive queries.
    """
    from ...services.salesforce_gateway import SalesforceGateway
    from ...db import get_session

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    soql = request.soql.strip()
    warnings = []
    estimated_rows = None

    # Parse SOQL for common issues
    soql_upper = soql.upper()

    # Check for WHERE clause on large objects
    large_objects = ["ACCOUNT", "CONTACT", "LEAD", "OPPORTUNITY", "CASE"]
    for obj in large_objects:
        if f"FROM {obj}" in soql_upper and "WHERE" not in soql_upper:
            warnings.append(
                f"No WHERE clause on {obj}. This may return many rows and use API calls."
            )

    # Check for LIMIT clause
    if "LIMIT" not in soql_upper:
        warnings.append(
            "No LIMIT clause. Consider adding LIMIT to control result size."
        )

    # Check for ORDER BY (recommended for pagination stability)
    if "LIMIT" in soql_upper and "ORDER BY" not in soql_upper:
        warnings.append(
            "LIMIT without ORDER BY may return inconsistent results across pages. Add ORDER BY Id."
        )

    # Check for child relationships (not supported in free tier)
    if "SELECT (" in soql:
        warnings.append(
            "Child relationship query detected. Use Join-Lite or upgrade to Pro for server-side joins."
        )

    async for db in get_session():
        try:
            gateway = await SalesforceGateway.for_tenant(str(account_id), db)

            # Resolve API limit headroom using live data when available,
            # otherwise fall back to the gateway's cached telemetry.
            def _coerce_int(value: Optional[Any]) -> Optional[int]:
                try:
                    return int(value)
                except (TypeError, ValueError):
                    return None

            limits_raw = None
            fetch_limits = getattr(gateway, "get_api_limits", None)
            if callable(fetch_limits):
                try:
                    limits_raw = await fetch_limits()
                except Exception:
                    # Silently fall back to cached values; live limits aren't always available.
                    limits_raw = None

            api_remaining = None
            api_max = None

            if isinstance(limits_raw, dict):
                daily_limits = limits_raw.get("DailyApiRequests") or limits_raw.get(
                    "daily_api_requests"
                )
                if isinstance(daily_limits, dict):
                    api_remaining = _coerce_int(
                        daily_limits.get("Remaining") or daily_limits.get("remaining")
                    )
                    api_max = _coerce_int(
                        daily_limits.get("Max") or daily_limits.get("max")
                    )
                    used = _coerce_int(
                        daily_limits.get("Used") or daily_limits.get("used")
                    )
                    if (
                        api_max is None
                        and api_remaining is not None
                        and used is not None
                    ):
                        api_max = api_remaining + used

            if api_max is None or api_remaining is None:
                parsed_usage = getattr(gateway, "_parsed_api_usage", None)
                if isinstance(parsed_usage, tuple) and len(parsed_usage) == 2:
                    used, total = parsed_usage
                    used_int = _coerce_int(used)
                    total_int = _coerce_int(total)
                    if total_int:
                        if api_max is None:
                            api_max = total_int
                        if api_remaining is None and used_int is not None:
                            api_remaining = max(total_int - used_int, 0)

            if api_remaining is None:
                cached_remaining = _coerce_int(
                    getattr(gateway, "_remaining_api_calls", None)
                )
                if cached_remaining is not None:
                    api_remaining = max(cached_remaining, 0)

            if api_max is None:
                api_max = 5000
            if api_remaining is None:
                api_remaining = max(int(api_max * 0.8), 1)

            # Estimate API calls needed (rough heuristic)
            projected_calls = 1  # Initial query
            if "LIMIT" in soql_upper:
                # Extract LIMIT value
                import re

                limit_match = re.search(r"LIMIT\s+(\d+)", soql_upper)
                if limit_match:
                    limit_val = int(limit_match.group(1))
                    # 2000 rows per API call
                    projected_calls = max(1, (limit_val // 2000) + 1)
            else:
                # Without LIMIT, assume potentially many calls
                projected_calls = 10
                warnings.append("Without LIMIT, query may use many API calls.")

            # Check if we'll hit 80% threshold
            will_hit_limit = (api_remaining - projected_calls) < (api_max * 0.2)

            if will_hit_limit:
                warnings.append(
                    f"Query will use ~{projected_calls} API calls. Org has {api_remaining} remaining (approaching 80% threshold)."
                )

            # If validate_only=False, try to get count estimate
            if not request.validate_only:
                # Try to get row count estimate (this is expensive, skip for now)
                pass

            return SOQLValidationResponse(
                valid=True,
                warnings=warnings,
                estimated_rows=estimated_rows,
                api_calls_remaining=api_remaining,
                projected_api_calls=projected_calls,
                will_hit_limit=will_hit_limit,
            )

        except Exception as e:
            return SOQLValidationResponse(
                valid=False,
                warnings=[f"Validation error: {str(e)}"],
                api_calls_remaining=0,
                projected_api_calls=0,
            )


@router.get("/quota")
async def get_quota_status(api_ctx=Depends(require_api_key)):
    """
    Get quota status for current user.
    """
    from ...middleware.quota_checker import get_quota
    from ...db import get_session

    account_id = api_ctx.account_id
    if not account_id:
        raise HTTPException(401, "Missing account_id")

    async for db in get_session():
        try:
            quota = await get_quota(str(account_id), db)
            limits = quota.get_limits()
            runs_limit = limits.get("daily_runs")
            if runs_limit is None:
                runs_limit = limits.get("monthly_rows")
            return {
                "ok": True,
                "data": {
                    "tier": quota.tier,
                    "runs_used": quota.daily_runs_used,
                    "runs_limit": runs_limit,
                    "rows_used": quota.daily_rows_used,
                    "reset_at": quota.reset_at.isoformat() if quota.reset_at else None,
                },
            }
        except Exception as e:
            raise HTTPException(500, f"Failed to get quota: {e}")


@router.get("/templates")
async def get_query_templates():
    """
    Return pre-built SOQL templates for common use cases.
    """
    templates = [
        {
            "id": "pipeline_by_stage",
            "name": "Pipeline by Stage",
            "description": "See open deals grouped by stage with totals",
            "soql": "SELECT StageName, COUNT(Id) DealCount, SUM(Amount) TotalValue FROM Opportunity WHERE IsClosed = false GROUP BY StageName ORDER BY SUM(Amount) DESC",
            "icon": "📊",
            "category": "Sales",
        },
        {
            "id": "meetings_this_week",
            "name": "Meetings This Week",
            "description": "This week's calendar events",
            "soql": "SELECT Subject, StartDateTime, WhoId, WhatId FROM Event WHERE StartDateTime >= THIS_WEEK ORDER BY StartDateTime",
            "icon": "📅",
            "category": "Activity",
        },
        {
            "id": "renewals_90d",
            "name": "Renewals in 90 Days",
            "description": "Upcoming renewal opportunities",
            "soql": "SELECT Name, AccountId, Amount, CloseDate, OwnerId FROM Opportunity WHERE CloseDate <= NEXT_N_DAYS:90 AND Type = 'Renewal' ORDER BY CloseDate",
            "icon": "🔄",
            "category": "Sales",
        },
        {
            "id": "new_leads_today",
            "name": "Today's New Leads",
            "description": "Leads created today",
            "soql": "SELECT Name, Company, Email, LeadSource, OwnerId FROM Lead WHERE CreatedDate = TODAY ORDER BY CreatedDate DESC",
            "icon": "✨",
            "category": "Leads",
        },
        {
            "id": "open_cases",
            "name": "Open Support Cases",
            "description": "Active cases by priority",
            "soql": "SELECT CaseNumber, Subject, Priority, Status, AccountId, OwnerId FROM Case WHERE IsClosed = false ORDER BY Priority, CreatedDate DESC LIMIT 200",
            "icon": "🎫",
            "category": "Service",
        },
    ]

    return {"ok": True, "data": {"templates": templates}}
