"""Tool: fetch_orchestration_templates -- fetch and save templates as JSON files."""

import json
import logging
import os
import re
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

from ..sfdc.cli import resolve_org, run_soql, SfCliError
from ..sfdc.soap_client import get_org_credentials, execute_apex, SoapApexError
from ..sfdc.apex_templates import build_export_apex

logger = logging.getLogger(__name__)

TOOL_NAME = "fetch_orchestration_templates"

TOOL_DEFINITION = {
    "name": TOOL_NAME,
    "description": (
        "Get, fetch, download, or export CloudSense orchestration process templates from a "
        "Salesforce org and save them as JSON files. "
        "Use this tool when the user says 'get templates', 'fetch templates', 'download templates', "
        "'export templates', 'pull templates', 'retrieve templates', or 'save templates'. "
        "When the user says 'get N templates' or 'get all templates', use THIS tool (not list). "
        "Templates are saved to cloudsense-dx-exports/orchestration-templates/<org>/<timestamp>/. "
        "You MUST provide either template_names OR set fetch_all to true. "
        "If neither is provided, the tool returns an error. "
        "Validates template names against the org before fetching and reports any that don't exist. "
        "Org is auto-resolved from session or workspace default if not specified."
    ),
    "inputSchema": {
        "type": "object",
        "properties": {
            "org_alias": {
                "type": "string",
                "description": (
                    "Salesforce org alias or partial hint (e.g. 'itxdevpro', 'devpro', 'sit p3'). "
                    "Partial names are fuzzy-matched against authenticated orgs. "
                    "OMIT this parameter if the user did not mention a specific org -- "
                    "the tool will automatically use the session org or workspace default."
                ),
            },
            "template_names": {
                "type": "array",
                "items": {"type": "string"},
                "description": (
                    "Specific template names to fetch, e.g. "
                    '["Internet CPE Terminate Flow (Subprocess)", "Callout to Inventory Reservation (Subprocess)"]. '
                    "Required unless fetch_all is true."
                ),
            },
            "fetch_all": {
                "type": "boolean",
                "description": (
                    "Set to true to fetch ALL orchestration process templates from the org. "
                    "Only use this when the user explicitly asks for all templates. "
                    "Cannot be combined with template_names."
                ),
                "default": False,
            },
            "max_workers": {
                "type": "integer",
                "description": "Number of parallel workers for concurrent fetching. Default: 8.",
                "default": 8,
            },
        },
        "required": [],
    },
}


def _sanitize_filename(name: str) -> str:
    sanitized = re.sub(r'[<>:"/\\|?*]', '_', name)
    sanitized = sanitized.replace(' ', '_')
    sanitized = sanitized.strip('._ ')
    return sanitized


_MAX_WORKERS_UPPER = int(os.getenv("CLOUDSENSE_DX_MAX_SF_CONCURRENCY", "8"))


def _clamp_workers(max_workers: Any) -> int:
    try:
        workers = int(max_workers)
    except (TypeError, ValueError):
        return 8
    return max(1, min(workers, _MAX_WORKERS_UPPER))


def _fetch_single_template(
    template_name: str,
    instance_url: str,
    access_token: str,
    api_version: str,
    output_dir: str,
    request_id: str = "",
) -> Dict:
    """Fetch one template via direct SOAP Apex call and save the JSON to disk."""
    t0 = time.monotonic()
    apex_code = build_export_apex(template_name)
    try:
        result = execute_apex(
            apex_code,
            instance_url=instance_url,
            access_token=access_token,
            api_version=api_version,
            timeout_seconds=120,
            request_id=request_id,
            template_name=template_name,
        )
    except SoapApexError as e:
        return {"name": template_name, "success": False, "error": str(e),
                "duration_s": round(time.monotonic() - t0, 2)}

    errors = result.extract_debug_lines("###ERROR###")
    if errors:
        return {"name": template_name, "success": False, "error": "; ".join(errors),
                "duration_s": round(time.monotonic() - t0, 2)}

    json_str = result.extract_chunks(prefix="###CHUNK_")
    if not json_str:
        return {
            "name": template_name,
            "success": False,
            "error": "No template data returned from Apex execution.",
            "duration_s": round(time.monotonic() - t0, 2),
        }

    sanitized = _sanitize_filename(template_name)
    filepath = Path(output_dir) / f"{sanitized}.json"
    try:
        filepath.write_text(json_str, encoding="utf-8")
        return {"name": template_name, "success": True, "file": str(filepath),
                "duration_s": round(time.monotonic() - t0, 2)}
    except Exception as e:
        return {"name": template_name, "success": False, "error": f"Failed to save file: {e}",
                "duration_s": round(time.monotonic() - t0, 2)}


def _query_all_template_names(org_alias: Optional[str]) -> Optional[List[str]]:
    soql = "SELECT Name FROM CSPOFA__Orchestration_Process_Template__c ORDER BY Name ASC"
    result = run_soql(soql, org_alias=org_alias)
    return [r["Name"] for r in result.get("records", []) if r.get("Name")]


def _escape_soql_string(value: str) -> str:
    """Escape a string for safe inclusion in a SOQL single-quoted literal."""
    return value.replace("\\", "\\\\").replace("'", "\\'")


def _validate_template_names(names: List[str], org_alias: Optional[str]) -> Dict:
    """Check which of the provided names actually exist in the org."""
    quoted = ", ".join(f"'{_escape_soql_string(n)}'" for n in names)
    soql = (
        "SELECT Name FROM CSPOFA__Orchestration_Process_Template__c "
        f"WHERE Name IN ({quoted})"
    )
    try:
        result = run_soql(soql, org_alias=org_alias)
    except SfCliError:
        return {"found": names, "not_found": [], "available": []}

    found_names = {r["Name"] for r in result.get("records", []) if r.get("Name")}
    not_found = [n for n in names if n not in found_names]

    available = []
    if not_found:
        all_names = _query_all_template_names(org_alias)
        if all_names:
            available = all_names

    return {
        "found": [n for n in names if n in found_names],
        "not_found": not_found,
        "available": available,
    }


async def execute(arguments: Dict[str, Any]) -> str:
    """Execute the fetch_orchestration_templates tool."""
    from uuid import uuid4

    start_time = time.monotonic()
    request_id = uuid4().hex[:12]
    template_names = arguments.get("template_names")
    fetch_all = arguments.get("fetch_all", False)
    max_workers = _clamp_workers(arguments.get("max_workers", 8))

    if template_names and fetch_all:
        return json.dumps({
            "success": False,
            "error": (
                "Cannot specify both template_names and fetch_all. "
                "Provide template_names for specific templates, or set fetch_all to true for all."
            ),
        }, indent=2)

    if not template_names and not fetch_all:
        return json.dumps({
            "success": False,
            "error": (
                "You must provide either template_names (list of specific names) "
                "or set fetch_all to true to fetch all templates. "
                "This prevents accidentally downloading every template in the org."
            ),
        }, indent=2)

    org_resolution = resolve_org(arguments.get("org_alias") or None)

    if "error" in org_resolution:
        return json.dumps({
            "success": False,
            "error": org_resolution["error"],
            "candidates": org_resolution.get("candidates", []),
        }, indent=2)

    org_alias = org_resolution["org_alias"]
    org_source = org_resolution["org_source"]

    # Get access token once for all parallel fetches
    try:
        creds = get_org_credentials(org_alias)
    except SoapApexError as e:
        return json.dumps({
            "success": False,
            "error": f"Failed to get org credentials: {e}",
            "org": org_alias,
            "org_source": org_source,
        }, indent=2)

    warnings: List[str] = []

    if fetch_all:
        try:
            all_names = _query_all_template_names(org_alias)
        except SfCliError as e:
            return json.dumps({
                "success": False,
                "error": str(e),
                "org": org_alias,
                "org_source": org_source,
            }, indent=2)
        if not all_names:
            return json.dumps({
                "success": True,
                "message": "No orchestration process templates found in this org.",
                "org": org_alias,
                "org_source": org_source,
                "templates_fetched": 0,
            }, indent=2)
        template_names = all_names
    else:
        validation = _validate_template_names(template_names, org_alias)
        if validation["not_found"]:
            not_found_msg = (
                f"Templates not found in org: {validation['not_found']}. "
                "They will be skipped."
            )
            if validation["available"]:
                not_found_msg += f" Available templates: {validation['available']}"
            warnings.append(not_found_msg)
            template_names = validation["found"]

        if not template_names:
            return json.dumps({
                "success": False,
                "error": "None of the specified template names exist in this org.",
                "org": org_alias,
                "org_source": org_source,
                "warnings": warnings,
            }, indent=2)

    org_folder = _sanitize_filename(org_alias)
    timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')

    export_root = Path(
        os.getenv("CLOUDSENSE_DX_EXPORT_ROOT", Path.cwd())
    )
    output_dir = export_root / "cloudsense-dx-exports" / "orchestration-templates" / org_folder / timestamp

    try:
        output_dir.mkdir(parents=True, exist_ok=True)
    except Exception as e:
        return json.dumps({
            "success": False,
            "error": f"Failed to create output directory '{output_dir}': {e}",
        }, indent=2)

    output_dir_str = str(output_dir)

    # Parallel fetch using direct SOAP calls (no subprocess per template)
    results = []
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_name = {
            executor.submit(
                _fetch_single_template,
                name,
                creds["instance_url"],
                creds["access_token"],
                creds["api_version"],
                output_dir_str,
                request_id,
            ): name
            for name in template_names
        }
        for future in as_completed(future_to_name):
            try:
                results.append(future.result())
            except Exception as exc:
                name = future_to_name[future]
                results.append({"name": name, "success": False, "error": str(exc)})

    elapsed = round(time.monotonic() - start_time, 1)
    succeeded = [r for r in results if r["success"]]
    failed = [r for r in results if not r["success"]]

    return json.dumps({
        "success": len(failed) == 0,
        "org": org_alias,
        "org_source": org_source,
        "output_directory": output_dir_str,
        "timestamp": timestamp,
        "elapsed_seconds": elapsed,
        "max_workers": max_workers,
        "summary": {
            "total_requested": len(results),
            "succeeded": len(succeeded),
            "failed": len(failed),
        },
        "succeeded": succeeded,
        "failed": failed,
        "warnings": warnings,
    }, indent=2)
