# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Smart Intake Skill — unified document/image/audio intake.

Workflow:
  1. Accept file (image, PDF, document, audio)
  2. Extract text (vision LLM → OCR fallback → PDF/DOCX reader → transcription)
  3. Classify content type (keyword heuristics → LLM refinement)
  4. Route to appropriate store (donors, grants, events, advocacy, notes)
  5. Record in history for audit trail

No new dependencies — uses existing platform capabilities:
  - Vision: Ollama vision models via /api/chat (httpx, core dep)
  - OCR: pytesseract + Pillow (docs extra, auto-installed)
  - PDF: PyMuPDF/fitz (docs extra)
  - Audio: faster-whisper (voice extra)
  - Classification: Ollama text models via /api/chat
"""

import logging
import shutil
import tempfile
from pathlib import Path

logger = logging.getLogger(__name__)

# ── Content type detection by file extension ──────────────────────────

_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".tiff", ".tif", ".bmp", ".gif", ".webp"}
_PDF_EXTS = {".pdf"}
_DOC_EXTS = {".docx", ".doc", ".xlsx", ".xls", ".csv", ".tsv", ".txt", ".md", ".rtf"}
_AUDIO_EXTS = {".mp3", ".wav", ".m4a", ".ogg", ".flac", ".aac", ".wma", ".opus"}
_ALL_SUPPORTED = _IMAGE_EXTS | _PDF_EXTS | _DOC_EXTS | _AUDIO_EXTS


def detect_file_type(filepath: str) -> str:
    """Detect intake file type from extension."""
    ext = Path(filepath).suffix.lower()
    if ext in _IMAGE_EXTS:
        return "image"
    if ext in _PDF_EXTS:
        return "pdf"
    if ext in _DOC_EXTS:
        return "document"
    if ext in _AUDIO_EXTS:
        return "audio"
    return "unknown"


# ── Text extraction by file type ──────────────────────────────────────


def _extract_from_image(filepath: Path, prefer_ocr: bool = False) -> str:
    """Extract text from image using vision LLM or OCR."""
    from familiar.skills.intake.vision import extract_text_from_image

    return extract_text_from_image(filepath, prefer_ocr=prefer_ocr)


def _extract_from_pdf(filepath: Path) -> str:
    """Extract text from PDF. Falls back to OCR for scanned PDFs."""
    from familiar.skills.filereader.skill import _read_pdf

    text = _read_pdf(filepath)

    # If PDF text extraction returned nothing useful, try OCR on first page
    if "no text extracted" in text.lower() or "scanned" in text.lower():
        from familiar.skills.intake.vision import extract_text_from_image

        try:
            import fitz

            doc = fitz.open(str(filepath))
            if len(doc) > 0:
                page = doc[0]
                pix = page.get_pixmap(dpi=200)
                # Use system temp dir for temp files — never pollute user dirs
                with tempfile.NamedTemporaryFile(
                    suffix=".png", prefix="intake_", delete=False
                ) as tmp:
                    tmp_path = Path(tmp.name)
                pix.save(str(tmp_path))
                doc.close()
                ocr_text = extract_text_from_image(tmp_path)
                # Always clean up
                try:
                    tmp_path.unlink()
                except OSError:
                    logger.debug("Failed to clean up temp file: %s", tmp_path)
                if ocr_text and "could not extract" not in ocr_text.lower():
                    return ocr_text
            else:
                doc.close()
        except ImportError:
            logger.debug("PyMuPDF not available for PDF-to-image conversion")
        except Exception as e:
            logger.debug("PDF-to-image OCR failed: %s", e)

    return text


def _extract_from_document(filepath: Path) -> str:
    """Extract text from DOCX, XLSX, CSV, TXT, etc."""
    from familiar.skills.filereader.skill import read_document

    return read_document({"filepath": str(filepath)})


def _check_ffmpeg() -> bool:
    """Check if ffmpeg is available (needed for non-WAV audio)."""
    return shutil.which("ffmpeg") is not None


def _extract_from_audio(filepath: Path) -> str:
    """Transcribe audio file. Checks ffmpeg for non-WAV formats."""
    ext = filepath.suffix.lower()
    needs_ffmpeg = ext != ".wav"

    if needs_ffmpeg and not _check_ffmpeg():
        return (
            f"Audio file ({ext}) requires ffmpeg for format conversion. "
            "Install with: sudo apt install ffmpeg"
        )

    try:
        from familiar.skills.transcription.skill import _transcribe_file

        text, backend = _transcribe_file(str(filepath))
        if text:
            return text
        return "No speech detected in audio file."
    except ImportError:
        return "Audio transcription unavailable. Install with: pip install faster-whisper"
    except Exception as e:
        err = str(e)
        # Give actionable error for common failures
        if "ffmpeg" in err.lower():
            return (
                "Audio transcription failed — ffmpeg is required. "
                "Install with: sudo apt install ffmpeg"
            )
        return f"Transcription error: {e}"


def extract_text(filepath: str, prefer_ocr: bool = False) -> tuple[str, str]:
    """
    Extract text from any supported file type.

    Returns:
        (extracted_text, file_type)
    """
    path = Path(filepath).expanduser()
    if not path.exists():
        return f"File not found: {filepath}", "unknown"

    file_type = detect_file_type(str(path))

    if file_type == "image":
        return _extract_from_image(path, prefer_ocr=prefer_ocr), file_type
    elif file_type == "pdf":
        return _extract_from_pdf(path), file_type
    elif file_type == "document":
        return _extract_from_document(path), file_type
    elif file_type == "audio":
        return _extract_from_audio(path), file_type
    else:
        # Try as text
        try:
            text = path.read_text(encoding="utf-8", errors="replace")[:50000]
            return text, "text"
        except Exception:
            return f"Unsupported file type: {path.suffix}", "unknown"


# ── Main intake pipeline ─────────────────────────────────────────────


def smart_intake(data: dict) -> str:
    """
    Main intake tool: extract → classify → route.

    Args (via data dict):
        filepath: Path to file to intake.
        dry_run: If true, classify but don't commit to store.
        prefer_ocr: If true, prefer Tesseract over vision LLM.
        content_type: Override auto-classification with specific type.
        skip_llm: If true, use keyword classification only.

    Returns:
        Human-readable summary of the intake result.
    """
    filepath = data.get("filepath", "").strip()
    if not filepath:
        return (
            "I need a file to scan. You can:\n"
            "1. Give me a file path (e.g. ~/Documents/receipt.jpg)\n"
            "2. Upload a file through the dashboard's Intake tab\n"
            "3. Take a photo and save it, then tell me the path\n\n"
            "Supported: images (JPG/PNG), PDFs, documents (DOCX/XLSX/CSV), "
            "audio (MP3/WAV/M4A)"
        )

    path = Path(filepath).expanduser()
    if not path.exists():
        return f"File not found: {filepath}"

    dry_run = data.get("dry_run", False)
    prefer_ocr = data.get("prefer_ocr", False)
    skip_llm = data.get("skip_llm", False)
    override_type = data.get("content_type", "")

    # Step 1: Extract text
    text, file_type = extract_text(str(path), prefer_ocr=prefer_ocr)

    if not text or text.startswith("File not found") or text.startswith("Unsupported"):
        return f"Could not extract text from {path.name}: {text}"

    # Step 2: Classify
    from familiar.skills.intake.classifiers import ContentType, classify

    if override_type:
        try:
            ct = ContentType(override_type)
            from familiar.skills.intake.classifiers import ClassificationResult

            result = ClassificationResult(
                content_type=ct,
                confidence=1.0,
                extracted_text=text,
                source_file=str(path),
                method="manual",
            )
            # Still try LLM for field extraction
            if not skip_llm:
                from familiar.skills.intake.classifiers import classify_with_llm

                llm_result = classify_with_llm(text, filepath=str(path))
                if llm_result and llm_result.extracted_fields:
                    result.extracted_fields = llm_result.extracted_fields
        except ValueError:
            return f"Unknown content type: {override_type}"
    else:
        result = classify(text, filepath=str(path), use_llm=not skip_llm)

    # Step 2.5: Flag for review if needed
    if not dry_run and result.needs_review:
        try:
            from familiar.skills.intake.review import get_review_queue

            queue = get_review_queue()
            queue.add_for_review(
                source_file=str(path),
                suggested_type=result.content_type.value,
                confidence=result.confidence,
                extracted_text=text,
                extracted_fields=result.extracted_fields,
                method=result.method,
            )
        except Exception as e:
            logger.debug("Failed to add to review queue: %s", e)

    # Step 3: Route
    from familiar.skills.intake.router import route

    route_result = route(result, dry_run=dry_run)

    # Step 4: Record in history
    if not dry_run:
        try:
            from familiar.skills.intake.history import get_intake_history

            history = get_intake_history()
            history.record(
                source_file=str(path),
                content_type=result.content_type.value,
                confidence=result.confidence,
                method=result.method,
                destination=route_result.destination,
                success=route_result.success,
                summary=route_result.summary,
                record_id=route_result.record_id,
                extracted_fields=result.extracted_fields,
                text_preview=text[:500],
            )
        except Exception as e:
            logger.warning("Failed to record intake history: %s", e)

    # Build response
    if dry_run:
        status = "preview"
    elif route_result.success:
        status = "committed"
    else:
        status = "failed"

    lines = [
        f"Intake: {path.name}",
        f"Type: {file_type} → {result.content_type.value.replace('_', ' ').title()}",
        f"Confidence: {result.confidence:.0%} ({result.method})",
        f"Route: {route_result.destination}",
        f"Status: {status}",
        f"Summary: {route_result.summary}",
    ]

    if route_result.file_id:
        lines.append(f"Indexed: FileStore ({route_result.file_id[:8]}…)")
    if route_result.memory_id:
        lines.append(f"Searchable: MemoryTree ({route_result.memory_id[:8]}…)")

    if result.extracted_fields:
        lines.append("Fields:")
        for k, v in result.extracted_fields.items():
            lines.append(f"  {k}: {v}")

    return "\n".join(lines)


def intake_history(data: dict) -> str:
    """View intake history and statistics."""
    from familiar.skills.intake.history import get_intake_history

    history = get_intake_history()
    action = data.get("action", "list")

    if action == "stats":
        stats = history.get_stats()
        if stats["total"] == 0:
            return "No intake history yet."

        lines = [
            "Intake Statistics:",
            f"  Total: {stats['total']}",
            f"  Success rate: {stats['success_rate']:.0%}",
            "",
            "By content type:",
        ]
        for ct, count in sorted(stats["by_type"].items(), key=lambda x: -x[1]):
            lines.append(f"  {ct}: {count}")
        lines.append("")
        lines.append("By destination:")
        for dest, count in sorted(stats["by_destination"].items(), key=lambda x: -x[1]):
            lines.append(f"  {dest}: {count}")
        return "\n".join(lines)

    # Default: list entries
    limit = int(data.get("limit", 20))
    content_type = data.get("content_type")
    entries = history.list_entries(limit=limit, content_type=content_type)

    if not entries:
        return "No intake history found."

    lines = [f"Recent intake ({len(entries)} entries):"]
    for e in entries:
        ts = e.get("timestamp", "")[:16].replace("T", " ")
        ok = "OK" if e.get("success") else "FAIL"
        lines.append(
            f"  [{ts}] {ok} | {e.get('content_type', '?')}"
            f" → {e.get('destination', '?')} | {e.get('summary', '')}"
        )

    return "\n".join(lines)


def batch_intake(data: dict) -> str:
    """Process multiple files through the intake pipeline."""
    filepaths = data.get("filepaths", [])
    directory = data.get("directory", "").strip()
    dry_run = data.get("dry_run", False)
    skip_llm = data.get("skip_llm", False)

    if directory:
        dir_path = Path(directory).expanduser()
        if not dir_path.is_dir():
            return f"Directory not found: {directory}"
        filepaths = [
            str(f)
            for f in sorted(dir_path.iterdir())
            if f.is_file() and f.suffix.lower() in _ALL_SUPPORTED
        ]

    if not filepaths:
        return "No files to process. Provide filepaths list or directory path."

    results = []
    for fp in filepaths[:50]:  # Cap at 50 files per batch
        result = smart_intake(
            {
                "filepath": fp,
                "dry_run": dry_run,
                "skip_llm": skip_llm,
            }
        )
        status_line = [line for line in result.split("\n") if line.startswith("Status:")]
        file_status = status_line[0].replace("Status: ", "") if status_line else "?"
        results.append(f"  {file_status} | {Path(fp).name}")

    lines = [f"Batch intake: {len(results)}/{len(filepaths)} files processed"]
    if dry_run:
        lines[0] += " (dry run)"
    lines.extend(results)
    return "\n".join(lines)


# ── Review, Watch, Clip, Template handlers ───────────────────────────


def review_intake(data: dict) -> str:
    """Review queue management — list, approve, or reject items."""
    from familiar.skills.intake.review import get_review_queue, process_review_decision

    action = data.get("action", "list")

    if action == "list":
        queue = get_review_queue()
        pending = queue.list_pending(limit=int(data.get("limit", 20)))
        if not pending:
            return "Review queue is empty — all classifications look good!"
        lines = [f"Review queue ({len(pending)} items):"]
        for item in pending:
            ts = item.get("timestamp", "")[:16].replace("T", " ")
            lines.append(
                f"  [{ts}] {item.get('id')} | {Path(item.get('source_file', '')).name}"
                f" → {item.get('suggested_type', '?')} ({item.get('confidence', 0):.0%})"
            )
        return "\n".join(lines)

    if action == "stats":
        queue = get_review_queue()
        stats = queue.get_stats()
        return (
            f"Review stats: {stats['pending']} pending, "
            f"{stats['approved']} approved, {stats['rejected']} rejected, "
            f"{stats['corrected']} corrected, accuracy {stats['accuracy_pct']:.1f}%"
        )

    if action in ("approve", "reject"):
        item_id = data.get("item_id", "").strip()
        if not item_id:
            return "item_id required for approve/reject."
        return process_review_decision(
            item_id=item_id,
            action=action,
            corrected_type=data.get("corrected_type"),
            corrected_fields=data.get("corrected_fields"),
            reason=data.get("reason", ""),
        )

    return f"Unknown action: {action}. Use 'list', 'stats', 'approve', or 'reject'."


def watch_folder(data: dict) -> str:
    """Manage watch folders — add, remove, list."""
    from familiar.skills.intake.watcher import get_folder_watcher

    watcher = get_folder_watcher()
    action = data.get("action", "list")

    if action == "add":
        directory = data.get("directory", "").strip()
        if not directory:
            return "directory required to add a watch."
        try:
            watch_id = watcher.add_watch(
                directory=directory,
                recursive=data.get("recursive", False),
                file_types=data.get("file_types"),
                label=data.get("label", ""),
            )
            return f"Watching: {directory} (ID: {watch_id})"
        except ValueError as e:
            return str(e)

    if action == "remove":
        watch_id = data.get("watch_id", "").strip()
        if not watch_id:
            return "watch_id required to remove a watch."
        if watcher.remove_watch(watch_id):
            return f"Removed watch: {watch_id}"
        return f"Watch not found: {watch_id}"

    # Default: list
    watches = watcher.list_watches()
    if not watches:
        return "No watch folders configured. Use action='add' to start monitoring a directory."
    lines = [f"Watch folders ({len(watches)}):"]
    for w in watches:
        checked = w.get("last_checked", "never")
        if checked and checked != "never":
            checked = checked[:16].replace("T", " ")
        lines.append(
            f"  {w.get('id')} | {w.get('label', '')} — {w.get('directory')}"
            f" | {w.get('files_processed', 0)} processed | last: {checked}"
        )
    return "\n".join(lines)


def check_watches(data: dict) -> str:
    """Check all watch folders for new files and auto-intake them."""
    from familiar.skills.intake.watcher import get_folder_watcher

    watcher = get_folder_watcher()
    new_files = watcher.check_watches()

    if not new_files:
        return "No new files found in watch folders."

    results = []
    for f_info in new_files:
        result = smart_intake(
            {
                "filepath": f_info["filepath"],
                "skip_llm": data.get("skip_llm", False),
            }
        )
        status_line = [line for line in result.split("\n") if line.startswith("Status:")]
        file_status = status_line[0].replace("Status: ", "") if status_line else "?"
        results.append(
            f"  {file_status} | {Path(f_info['filepath']).name}"
            f" (from {f_info.get('watch_label', '?')})"
        )

    lines = [f"Watch check: {len(results)} new files processed"]
    lines.extend(results)
    return "\n".join(lines)


def clip_webpage(data: dict) -> str:
    """Clip a web page — fetch, extract text, classify, and route."""
    from familiar.skills.intake.clipper import get_web_clipper

    url = data.get("url", "").strip()
    if not url:
        return "I need a URL to clip. What page should I save?"

    dry_run = data.get("dry_run", False)
    clipper = get_web_clipper()
    result = clipper.clip(url=url, dry_run=dry_run)

    if result.get("error"):
        return result["error"]

    lines = [
        f"Clipped: {result.get('title', 'untitled')}",
        f"Source: {url}",
        f"Type: {result.get('content_type', '?').replace('_', ' ').title()}",
        f"Confidence: {result.get('confidence', 0):.0%}",
    ]
    if result.get("destination"):
        lines.append(f"Routed to: {result['destination']}")
    if dry_run:
        lines.append("(dry run — not saved)")
    if result.get("text_preview"):
        lines.append(f"Preview: {result['text_preview'][:200]}...")

    return "\n".join(lines)


def list_templates(data: dict) -> str:
    """List extraction templates learned from corrections."""
    from familiar.skills.intake.templates import get_template_store

    store = get_template_store()
    templates = store.list_templates(limit=int(data.get("limit", 20)))

    if not templates:
        return "No extraction templates yet. Templates are learned when you correct classifications in the review queue."

    lines = [f"Extraction templates ({len(templates)}):"]
    for t in templates:
        lines.append(
            f"  {t.get('id')} | {t.get('content_type')} | "
            f"{len(t.get('patterns', []))} patterns | "
            f"used {t.get('use_count', 0)} times"
        )
    return "\n".join(lines)


# ── Tool Definitions ──────────────────────────────────────────────────

TOOLS = [
    {
        "name": "smart_intake",
        "description": (
            "Scan and process a document, image, or audio file. Automatically "
            "extracts text (vision AI for photos, OCR for scans, transcription "
            "for audio), classifies the content (receipt, grant, invoice, event "
            "flyer, legislation, business card, meeting notes, recipe, etc.), "
            "and saves it to the right place. Works with JPG, PNG, PDF, DOCX, "
            "XLSX, CSV, MP3, WAV, and more. Use this when the user says things "
            "like 'scan this', 'process this document', 'read this receipt', or "
            "'what does this say'. If the user hasn't provided a file path, ask "
            "them for one or suggest they upload through the dashboard."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "filepath": {
                    "type": "string",
                    "description": "Path to the file to scan/intake",
                },
                "dry_run": {
                    "type": "boolean",
                    "default": False,
                    "description": "Preview what would happen without saving",
                },
                "prefer_ocr": {
                    "type": "boolean",
                    "default": False,
                    "description": "Use OCR instead of vision AI (faster for printed text)",
                },
                "content_type": {
                    "type": "string",
                    "description": (
                        "Override auto-detection: donation_receipt, grant_document, "
                        "invoice, event_flyer, legislation, meeting_notes, "
                        "business_card, recipe, general_document"
                    ),
                },
                "skip_llm": {
                    "type": "boolean",
                    "default": False,
                    "description": "Skip AI classification, use keyword matching only",
                },
            },
            "required": ["filepath"],
        },
        "handler": smart_intake,
        "category": "intake",
    },
    {
        "name": "intake_history",
        "description": (
            "Show what documents have been scanned recently and intake statistics. "
            "Use action='stats' for a summary of how many documents were processed "
            "by type and where they were routed."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["list", "stats"],
                    "default": "list",
                    "description": "'list' for recent scans, 'stats' for totals",
                },
                "limit": {
                    "type": "integer",
                    "default": 20,
                    "description": "How many entries to show",
                },
                "content_type": {
                    "type": "string",
                    "description": "Filter by type (e.g. 'donation_receipt')",
                },
            },
        },
        "handler": intake_history,
        "category": "intake",
    },
    {
        "name": "batch_intake",
        "description": (
            "Scan a whole folder of documents at once. Point it at a directory "
            "and it will process every supported file (images, PDFs, docs, audio). "
            "Each file is classified and routed independently. Max 50 files."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "filepaths": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of file paths to process",
                },
                "directory": {
                    "type": "string",
                    "description": "Directory to scan for supported files",
                },
                "dry_run": {
                    "type": "boolean",
                    "default": False,
                    "description": "Preview without saving",
                },
                "skip_llm": {
                    "type": "boolean",
                    "default": False,
                    "description": "Skip AI classification",
                },
            },
        },
        "handler": batch_intake,
        "category": "intake",
    },
    {
        "name": "review_intake",
        "description": (
            "Review queue for document classifications that need human verification. "
            "List pending items, approve (optionally with corrected type), or reject. "
            "Corrections teach the system via extraction templates."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["list", "stats", "approve", "reject"],
                    "default": "list",
                    "description": "Action to perform on the review queue",
                },
                "item_id": {
                    "type": "string",
                    "description": "Review item ID (required for approve/reject)",
                },
                "corrected_type": {
                    "type": "string",
                    "description": "Correct content type on approve (e.g. 'donation_receipt')",
                },
                "corrected_fields": {
                    "type": "object",
                    "description": "Corrected extracted fields on approve",
                },
                "reason": {
                    "type": "string",
                    "description": "Reason for rejection",
                },
                "limit": {
                    "type": "integer",
                    "default": 20,
                    "description": "Max items to show for list action",
                },
            },
        },
        "handler": review_intake,
        "category": "intake",
    },
    {
        "name": "watch_folder",
        "description": (
            "Manage watch folders — directories monitored for new files that are "
            "automatically processed through Smart Intake. Use action='add' to start "
            "watching a directory, 'remove' to stop, or 'list' to see active watches."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["list", "add", "remove"],
                    "default": "list",
                    "description": "Action to perform",
                },
                "directory": {
                    "type": "string",
                    "description": "Directory path to watch (for add action)",
                },
                "watch_id": {
                    "type": "string",
                    "description": "Watch ID (for remove action)",
                },
                "recursive": {
                    "type": "boolean",
                    "default": False,
                    "description": "Watch subdirectories too",
                },
                "file_types": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Limit to specific extensions (e.g. ['.pdf', '.jpg'])",
                },
                "label": {
                    "type": "string",
                    "description": "Friendly label for the watch folder",
                },
            },
        },
        "handler": watch_folder,
        "category": "intake",
    },
    {
        "name": "check_watches",
        "description": (
            "Check all watch folders for new files and auto-intake them. "
            "New files are detected by hash — already-seen files are skipped. "
            "Max 100 files per check cycle."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "skip_llm": {
                    "type": "boolean",
                    "default": False,
                    "description": "Skip AI classification for speed",
                },
            },
        },
        "handler": check_watches,
        "category": "intake",
    },
    {
        "name": "clip_webpage",
        "description": (
            "Clip a web page — fetches the URL, extracts readable text, saves as "
            "Markdown, classifies the content, and routes it through the intake pipeline. "
            "Great for saving grant listings, resource pages, articles, or recipes from the web."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "URL of the web page to clip",
                },
                "dry_run": {
                    "type": "boolean",
                    "default": False,
                    "description": "Preview without saving",
                },
            },
            "required": ["url"],
        },
        "handler": clip_webpage,
        "category": "intake",
    },
    {
        "name": "list_templates",
        "description": (
            "List extraction templates learned from review corrections. Templates "
            "match text patterns to classify documents faster and extract fields "
            "without needing an LLM call."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "limit": {
                    "type": "integer",
                    "default": 20,
                    "description": "Max templates to show",
                },
            },
        },
        "handler": list_templates,
        "category": "intake",
    },
]
