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

import json
import logging
import os
import re
import asyncio
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Awaitable, Callable, Dict, List, Optional

from ..sfdc.cli import resolve_org_async, run_soql_async, run_apex, SfCliError
from ..sfdc.apex_templates import build_export_apex

logger = logging.getLogger(__name__)

ProgressCallback = Optional[Callable[[int, int, str], Awaitable[None]]]

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, org_alias: Optional[str], output_dir: str) -> Dict:
    """Fetch one template via anonymous Apex and save the JSON result to disk."""
    apex_code = build_export_apex(template_name)
    try:
        apex_result = run_apex(apex_code, org_alias=org_alias, timeout_seconds=120)
    except SfCliError as e:
        return {"name": template_name, "success": False, "error": str(e)}

    errors = apex_result.extract_debug_lines("###ERROR###")
    if errors:
        return {"name": template_name, "success": False, "error": "; ".join(errors)}

    json_str = apex_result.extract_chunks(prefix="###CHUNK_")
    if not json_str:
        return {
            "name": template_name,
            "success": False,
            "error": "No template data returned from Apex execution.",
        }

    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)}
    except Exception as e:
        return {"name": template_name, "success": False, "error": f"Failed to save file: {e}"}


async 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 = await run_soql_async(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("'", "\\'")


async 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 = await run_soql_async(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 = await _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 _fetch_single_template_async(
    template_name: str,
    org_alias: Optional[str],
    output_dir: str,
    semaphore: asyncio.Semaphore,
) -> Dict:
    """Fetch one template, bounded by semaphore for concurrency control."""
    async with semaphore:
        try:
            return await asyncio.to_thread(
                _fetch_single_template, template_name, org_alias, output_dir
            )
        except Exception as exc:
            return {"name": template_name, "success": False, "error": str(exc)}


async def _fetch_all_async(
    template_names: List[str],
    org_alias: Optional[str],
    output_dir: str,
    max_workers: int,
    progress_callback: ProgressCallback = None,
) -> List[Dict[str, Any]]:
    """Fetch templates concurrently with per-completion progress notifications."""
    sem = asyncio.Semaphore(max_workers)
    total = len(template_names)
    completed = 0

    async def _fetch_with_progress(name: str) -> Dict:
        nonlocal completed
        result = await _fetch_single_template_async(name, org_alias, output_dir, sem)
        completed += 1
        if progress_callback:
            status = "ok" if result.get("success") else "failed"
            try:
                await progress_callback(
                    completed, total,
                    f"Fetched {completed}/{total}: {name} ({status})",
                )
            except Exception:
                logger.debug("Progress callback failed for %s", name)
        return result

    return list(await asyncio.gather(
        *[_fetch_with_progress(name) for name in template_names]
    ))


async def execute(arguments: Dict[str, Any], progress_callback: ProgressCallback = None) -> str:
    """Execute the fetch_orchestration_templates tool."""
    start_time = time.monotonic()
    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 = await resolve_org_async(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"]

    warnings: List[str] = []

    if fetch_all:
        try:
            all_names = await _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
        # Skip validation -- names came directly from the org
    else:
        validation = await _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)

    results = await _fetch_all_async(
        template_names, org_alias, output_dir_str, max_workers, progress_callback
    )

    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,
        "summary": {
            "total_requested": len(results),
            "succeeded": len(succeeded),
            "failed": len(failed),
        },
        "succeeded": succeeded,
        "failed": failed,
        "warnings": warnings,
    }, indent=2)
