#!/usr/bin/env python3
"""
zoho_webhook.py (final)

Features:
- Accepts webhook via GET (query params) or POST (json/form)
- Verifies webhook secret from header / json / query
- Fetch ATS record, extract email + attachments (including "Others")
- Download attachments to disk and optionally upload to Candidate (Resume)
- Can create Candidate from ATS record if not found
- CLI helpers: get_token_only, check_ats, create_candidate_from_ats, download_attachment
- Safe tokens.json atomic writes
"""

import re
import os
import json
import time
import logging
import tempfile
import argparse
from pathlib import Path
from datetime import datetime
from typing import Optional, List, Tuple, Any

import requests
from flask import Flask, request, jsonify, abort

# ---------------------------
# Config - عدّل القيم هنا
# ---------------------------
CLIENT_ID = os.environ.get("ZOHO_CLIENT_ID", "1000.6XRABP1ZPTQ2M0TOCLMUA3764Y0B1Z")
CLIENT_SECRET = os.environ.get("ZOHO_CLIENT_SECRET", "REPLACE_ME")
AUTH_CODE = os.environ.get("ZOHO_AUTH_CODE", "")  # optional one-time
REDIRECT_URI = "https://www.zoho.com"
TOKENS_FILE = Path("/var/www/zoho/tokens.json")
RECRUIT_API_BASE = "https://recruit.zoho.com/recruit/v2"

# tmp and attach dirs
TMP_DIR = Path("/var/www/zoho/tmp")
TMP_DIR.mkdir(parents=True, exist_ok=True)
ATTACH_DIR = Path("/var/www/zoho/attache")
ATTACH_DIR.mkdir(parents=True, exist_ok=True)

# logs
LOG_DIR = Path("/var/www/zoho/Log")
LOG_DIR.mkdir(parents=True, exist_ok=True)
LOG_FILE = LOG_DIR / "webhook_transfer.log"
RECEIVED_LOG = LOG_DIR / "webhook_received.log"
ERROR_LOG = LOG_DIR / "webhook_error.log"

# webhook secret
WEBHOOK_SECRET = os.environ.get("ZOHO_WEBHOOK_SECRET", "change_this_shared_secret")

# timeouts/retries
MAX_DOWNLOAD_RETRIES = 3
MAX_UPLOAD_RETRIES = 6
REQUEST_TIMEOUT = 120

# ---------------------------
# Logging configuration
# ---------------------------
logger = logging.getLogger("zoho_webhook")
logger.setLevel(logging.INFO)

try:
    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
    LOG_FILE.touch(exist_ok=True)
    ERROR_LOG.touch(exist_ok=True)
    RECEIVED_LOG.touch(exist_ok=True)
except Exception:
    pass

fh = logging.FileHandler(str(LOG_FILE))
fh.setLevel(logging.INFO)
fh_fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
fh.setFormatter(fh_fmt)
logger.addHandler(fh)

eh = logging.FileHandler(str(ERROR_LOG))
eh.setLevel(logging.ERROR)
eh.setFormatter(fh_fmt)
logger.addHandler(eh)

ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(fh_fmt)
logger.addHandler(ch)


def current_timestamp() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def append_received_log(payload: Any):
    try:
        entry = {"ts": current_timestamp(), "payload": payload}
        with RECEIVED_LOG.open("a", encoding="utf-8") as f:
            f.write(json.dumps(entry, ensure_ascii=False) + "\n")
    except Exception as e:
        logger.error(f"Could not write to received log: {e}", exc_info=True)


# ---------------------------
# Token helpers (atomic write)
# ---------------------------
def load_tokens() -> Optional[dict]:
    if not TOKENS_FILE.exists():
        logger.info(f"tokens.json not found at {TOKENS_FILE}")
        return None
    try:
        return json.loads(TOKENS_FILE.read_text(encoding="utf-8"))
    except Exception as e:
        logger.warning(f"failed to read tokens.json: {e}", exc_info=True)
        return None


def save_tokens(tokens: dict):
    TOKENS_FILE.parent.mkdir(parents=True, exist_ok=True)
    old = load_tokens() or {}
    merged = old.copy()
    merged.update(tokens)
    # keep existing refresh token if new doesn't include it
    if "refresh_token" not in tokens and "refresh_token" in old:
        merged["refresh_token"] = old["refresh_token"]
    fd, tmp_path = tempfile.mkstemp(dir=str(TOKENS_FILE.parent))
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(json.dumps(merged, indent=2))
        os.replace(tmp_path, TOKENS_FILE)
        try:
            TOKENS_FILE.chmod(0o600)
        except Exception:
            logger.debug("Could not chmod tokens.json (ignored).")
    except Exception as e:
        logger.exception("Failed atomic write for tokens.json")
        try:
            TOKENS_FILE.write_text(json.dumps(merged, indent=2), encoding="utf-8")
        except Exception as e2:
            logger.exception("Fallback write for tokens.json failed")


def exchange_auth_code_for_tokens(auth_code: str):
    url = "https://accounts.zoho.com/oauth/v2/token"
    data = {
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "grant_type": "authorization_code",
        "code": auth_code,
        "redirect_uri": REDIRECT_URI,
    }
    r = requests.post(url, data=data, timeout=30)
    if r.status_code != 200:
        raise Exception(f"Auth code exchange failed ({r.status_code}): {r.text}")
    tokens = r.json()
    save_tokens(tokens)
    logger.info("Auth code exchanged and tokens saved")
    return tokens


def refresh_access_token() -> str:
    tokens = load_tokens()
    if not tokens:
        raise Exception("tokens.json not found. Place tokens.json or set AUTH_CODE and run exchange.")
    refresh_token = tokens.get("refresh_token")
    if not refresh_token:
        raise Exception("No refresh_token in tokens.json. Run the AUTH_CODE flow first.")
    url = "https://accounts.zoho.com/oauth/v2/token"
    data = {
        "refresh_token": refresh_token,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "grant_type": "refresh_token"
    }
    r = requests.post(url, data=data, timeout=30)
    if r.status_code != 200:
        logger.error(f"refresh_access_token failed: {r.status_code} {r.text}")
        raise Exception(f"Refresh failed ({r.status_code}): {r.text}")
    new_tokens = r.json()
    save_tokens(new_tokens)
    logger.info("Access token refreshed and saved to tokens.json")
    return new_tokens["access_token"]


def get_access_token(allow_exchange_if_missing=True) -> str:
    tokens = load_tokens()
    if not tokens:
        if AUTH_CODE and allow_exchange_if_missing:
            tokens = exchange_auth_code_for_tokens(AUTH_CODE)
        else:
            raise Exception("tokens.json not found and AUTH_CODE not provided.")
    access_token = tokens.get("access_token")
    if not access_token:
        access_token = refresh_access_token()
    return access_token


# ---------------------------
# Zoho helper functions
# ---------------------------
def search_candidate_by_email(email: str) -> Optional[str]:
    if not email:
        return None
    for attempt in range(2):
        token = get_access_token(allow_exchange_if_missing=(attempt == 0))
        headers = {"Authorization": f"Zoho-oauthtoken {token}"}
        criteria = f"(Email:equals:{email})"
        url = f"{RECRUIT_API_BASE}/Candidates/search?criteria={requests.utils.quote(criteria)}"
        try:
            r = requests.get(url, headers=headers, timeout=30)
        except Exception as e:
            logger.error(f"search_candidate_by_email request failed: {e}", exc_info=True)
            return None
        if r.status_code == 200:
            data = r.json().get("data")
            if data:
                return data[0].get("id")
            return None
        if r.status_code == 204:
            return None
        if r.status_code == 401:
            logger.info("search_candidate_by_email: 401 received, refreshing token")
            refresh_access_token()
            continue
        logger.warning(f"search candidate returned {r.status_code}: {r.text}")
        return None
    return None


def create_candidate_from_ats(ats_id: str, module_name: str = "CustomModule4") -> Optional[str]:
    logger.info(f"Attempting to create Candidate from ATS {ats_id} (module={module_name})")
    record = fetch_ats_record(ats_id)
    if not record:
        logger.error(f"ATS record {ats_id} not found or empty")
        return None

    email = record.get("Email") or record.get("email")
    ats_name = (record.get(f"{module_name} Name")
                or record.get("CustomModule4 Name")
                or record.get("Name")
                or record.get("Full_Name")
                or record.get("custom_name"))
    department = record.get("Department") or record.get("department")

    logger.info(f"Extracted from ATS: email={email}, name={ats_name}, department={department}")

    if email:
        existing = search_candidate_by_email(email)
        if existing:
            logger.info(f"Candidate already exists with email {email}: id={existing}")
            return existing

    candidate_payload = {}
    if ats_name:
        candidate_payload["Last Name"] = ats_name
    if email:
        candidate_payload["Email"] = email
    candidate_payload["Source"] = "ATS Module"
    if department:
        candidate_payload["Department"] = department

    if not candidate_payload.get("Last Name") and not candidate_payload.get("Email"):
        logger.error("Insufficient data to create candidate (no name and no email)")
        return None

    token = get_access_token()
    url = f"{RECRUIT_API_BASE}/Candidates"
    headers = {"Authorization": f"Zoho-oauthtoken {token}", "Content-Type": "application/json"}
    body = {"data": [candidate_payload]}

    try:
        r = requests.post(url, headers=headers, json=body, timeout=REQUEST_TIMEOUT)
    except Exception as e:
        logger.exception(f"Failed to POST candidate to Zoho: {e}")
        return None

    if r.status_code in (200, 201):
        try:
            jr = r.json()
            data = jr.get("data")
            cid = None
            if isinstance(data, list) and data:
                first = data[0]
                cid = first.get("details", {}).get("id") or first.get("details", {}).get("recordId") or first.get("id")
            if not cid:
                cid = jr.get("id") or jr.get("candidate_id")
            logger.info(f"Candidate created successfully, response id: {cid}")
            return cid
        except Exception:
            logger.info(f"Candidate created (status {r.status_code}) but couldn't parse id; raw: {r.text}")
            return None
    else:
        if r.status_code == 401:
            logger.info("create_candidate_from_ats: got 401, refreshing token and retrying once")
            try:
                refresh_access_token()
                token = get_access_token()
                headers["Authorization"] = f"Zoho-oauthtoken {token}"
                r2 = requests.post(url, headers=headers, json=body, timeout=REQUEST_TIMEOUT)
                if r2.status_code in (200, 201):
                    try:
                        jr = r2.json()
                        data = jr.get("data")
                        cid = None
                        if isinstance(data, list) and data:
                            first = data[0]
                            cid = first.get("details", {}).get("id") or first.get("id")
                        logger.info(f"Candidate created on retry, id: {cid}")
                        return cid
                    except Exception:
                        logger.info(f"Candidate created on retry but couldn't parse id; raw: {r2.text}")
                        return None
                logger.error(f"Retry create candidate failed: {r2.status_code} {r2.text}")
                return None
            except Exception as e:
                logger.exception(f"Retry refresh failed: {e}")
                return None
        logger.error(f"Failed to create candidate: {r.status_code} {r.text}")
        return None


def extract_attachment_ids_from_json(obj: Any) -> List[str]:
    found: List[str] = []
    if isinstance(obj, dict):
        for k, v in obj.items():
            if isinstance(v, (str, int)) and k.lower() in ("id", "attachment_id", "attachmentid", "file_id", "fileid"):
                found.append(str(v))
            if isinstance(v, list) and k.lower() in ("attachments", "files", "documents"):
                for item in v:
                    if isinstance(item, dict):
                        aid = item.get("id") or item.get("attachment_id") or item.get("file_id") or item.get("attachmentId")
                        if aid:
                            found.append(str(aid))
                        else:
                            found.extend(extract_attachment_ids_from_json(item))
                    elif isinstance(item, (str, int)):
                        found.append(str(item))
            if isinstance(v, (dict, list)):
                found.extend(extract_attachment_ids_from_json(v))
    elif isinstance(obj, list):
        for item in obj:
            found.extend(extract_attachment_ids_from_json(item))
    return list(dict.fromkeys(found))


def fetch_ats_record(ats_record_id: str) -> dict:
    token = get_access_token()
    headers = {"Authorization": f"Zoho-oauthtoken {token}"}
    url = f"{RECRUIT_API_BASE}/ATS/{ats_record_id}"
    try:
        r = requests.get(url, headers=headers, timeout=30)
    except Exception as e:
        logger.error(f"fetch_ats_record request failed: {e}", exc_info=True)
        return {}
    if r.status_code == 200:
        data = r.json().get("data") or []
        record = data[0] if isinstance(data, list) and data else (data if isinstance(data, dict) else {})
        return record
    elif r.status_code == 204:
        logger.info(f"ATS record {ats_record_id} returned 204 (no content)")
        return {}
    elif r.status_code == 401:
        logger.info("fetch_ats_record: got 401, refreshing token and retrying once")
        try:
            refresh_access_token()
            return fetch_ats_record(ats_record_id)
        except Exception as e:
            logger.error(f"refresh failed: {e}", exc_info=True)
            return {}
    else:
        logger.warning(f"fetch_ats_record returned {r.status_code}: {r.text}")
        return {}


def get_email_and_attachments_from_ats(ats_record_id: str) -> Tuple[Optional[str], List[str]]:
    record = fetch_ats_record(ats_record_id)
    if not record:
        return None, []

    # Try common places for email
    email = None
    for key in ("Email", "email", "Applicant_Email", "applicant_email", "candidate_email", "ApplicantEmail"):
        v = record.get(key)
        if isinstance(v, str) and v.strip():
            email = v.strip()
            break

    # nested applicant info
    if not email:
        for k, v in record.items():
            if isinstance(v, dict):
                for kk in ("email", "Email"):
                    ev = v.get(kk)
                    if isinstance(ev, str) and ev.strip():
                        email = ev.strip()
                        break
                if email:
                    break

    # attachments in Others or scan whole record
    attachments: List[str] = []
    for k in record.keys():
        if k.lower() == "others" or k.lower().startswith("other"):
            others = record.get(k)
            if isinstance(others, dict):
                for fk in ("attachments", "Attachments", "files", "Files", "documents", "Documents"):
                    files_val = others.get(fk)
                    if isinstance(files_val, list):
                        for f in files_val:
                            if isinstance(f, dict):
                                fid = f.get("id") or f.get("attachment_id") or f.get("file_id") or f.get("attachmentId")
                                if fid:
                                    attachments.append(str(fid))
                            elif isinstance(f, (str, int)):
                                attachments.append(str(f))
            elif isinstance(others, list):
                attachments.extend(extract_attachment_ids_from_json(others))

    if not attachments:
        attachments = extract_attachment_ids_from_json(record)

    attachments = list(dict.fromkeys(attachments))
    return email, attachments


def sanitize_filename(name: str) -> str:
    name = name.strip()
    name = re.sub(r"[:\\/]+", "_", name)
    name = re.sub(r"[^0-9A-Za-z._- ]+", "", name)
    name = name[:200]
    return name


def download_file_from_ats_to_disk(file_id: str, dest_dir: Path) -> Tuple[Path, str]:
    if not file_id:
        raise ValueError("file_id required")
    for attempt in range(MAX_DOWNLOAD_RETRIES):
        token = get_access_token()
        url = f"{RECRUIT_API_BASE}/ATS/Attachments/{file_id}/download"
        headers = {"Authorization": f"Zoho-oauthtoken {token}"}
        r = requests.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
        if r.status_code == 200:
            cd = r.headers.get("Content-Disposition", "")
            m = re.search(r'filename="([^"]+)"', cd)
            filename = m.group(1) if m else f"{file_id}.bin"
            filename = sanitize_filename(filename)
            dest_dir.mkdir(parents=True, exist_ok=True)
            out_path = dest_dir / f"{int(time.time())}_{filename}"
            try:
                with open(out_path, "wb") as f:
                    f.write(r.content)
            except Exception as e:
                logger.warning(f"Could not write to disk {out_path}: {e}", exc_info=True)
            return out_path, filename
        if r.status_code == 401:
            logger.info("download_file_from_ats_to_disk: received 401, refreshing token")
            refresh_access_token()
            continue
        if r.status_code in (429, 503, 500):
            wait = min(10 * (attempt + 1), 120)
            logger.warning(f"download {file_id} returned {r.status_code}. retrying after {wait}s")
            time.sleep(wait)
            continue
        raise Exception(f"Failed to download {file_id}: {r.status_code} {r.text}")
    raise Exception(f"Failed to download {file_id} after retries")


def upload_file_to_candidate(candidate_id: str, file_path: Path, file_name: str,
                             category: str = "Resume") -> Tuple[int, str]:
    url = f"{RECRUIT_API_BASE}/Candidates/{candidate_id}/Attachments"
    retries = 0
    last_resp = None
    while retries <= MAX_UPLOAD_RETRIES:
        token = get_access_token()
        headers = {"Authorization": f"Zoho-oauthtoken {token}"}
        data = {"attachments_category": category}
        try:
            with file_path.open("rb") as fh:
                files = {"file": (file_name, fh)}
                r = requests.post(url, headers=headers, files=files, data=data, timeout=REQUEST_TIMEOUT)
        except Exception as e:
            logger.error(f"upload exception for {file_name}: {e}", exc_info=True)
            time.sleep(min(60 * (retries + 1), 600))
            retries += 1
            continue
        last_resp = r
        if r.status_code in (200, 201):
            return r.status_code, r.text
        if r.status_code == 401:
            logger.info("upload_file_to_candidate: got 401, refreshing token")
            refresh_access_token()
            retries += 1
            continue
        if r.status_code == 429:
            wait_time = min(60 * (retries + 1), 900)
            logger.warning(f"429 Too Many Requests for {file_name}. Sleeping {wait_time}s")
            time.sleep(wait_time)
            retries += 1
            continue
        if 500 <= r.status_code < 600:
            wait_time = min(30 * (retries + 1), 300)
            logger.warning(f"Server error {r.status_code} for {file_name}. Sleeping {wait_time}s")
            time.sleep(wait_time)
            retries += 1
            continue
        return r.status_code, r.text
    return (last_resp.status_code if last_resp is not None else 0,
            last_resp.text if last_resp is not None else "")


# ---------------------------
# Flask app - endpoints
# ---------------------------
app = Flask(__name__)


def parse_attachment_ids(payload: dict) -> List[str]:
    out = []
    for key in ("attachment_ids", "file_ids", "Attachment_IDs", "attachmentIds", "attachments", "attachment_ids_csv"):
        if key in payload:
            val = payload.get(key)
            if isinstance(val, list):
                out.extend([str(v) for v in val if v])
            elif isinstance(val, str) and val.strip():
                out.extend([p.strip() for p in val.split(",") if p.strip()])
    att = payload.get("attachments")
    if isinstance(att, list):
        for a in att:
            if isinstance(a, dict):
                aid = a.get("id") or a.get("attachment_id") or a.get("attachmentId")
                if aid:
                    out.append(str(aid))
    return list(dict.fromkeys(out))


@app.route("/zoho_webhook", methods=["GET", "POST"])
def zoho_webhook():
    # Accept JSON, form-data or query params
    payload: dict = {}
    try:
        # merge GET query params first (so JSON/form can override)
        if request.args:
            for k in request.args:
                # take first value
                payload[k] = request.args.get(k)
        if request.method == "POST":
            if request.is_json:
                body = request.get_json()
                if isinstance(body, dict):
                    payload.update(body)
            else:
                # form data (may contain arrays)
                form = request.form.to_dict(flat=False)
                for k, v in form.items():
                    payload[k] = v if len(v) > 1 else v[0]
    except Exception as e:
        logger.error("Failed to parse incoming payload", exc_info=True)
        append_received_log({"raw": request.get_data(as_text=True)})
        return jsonify({"status": "bad_payload", "error": str(e)}), 400

    append_received_log(payload)
    logger.info(f"Webhook received: {payload}")

    # verify secret: header, payload, or query param
    secret_header = request.headers.get("X-Webhook-Secret") or request.headers.get("X-ZOHO-SECRET")
    provided_secret = payload.get("webhook_secret") or secret_header
    if WEBHOOK_SECRET and provided_secret != WEBHOOK_SECRET:
        logger.warning("Invalid webhook secret")
        append_received_log({"invalid_secret_payload": payload})
        abort(403, "Forbidden")

    # accept several key names for ATS id
    ats_record_id = (payload.get("ATS_Record_ID") or payload.get("ats_record_id")
                     or payload.get("record_id") or payload.get("ats_id") or payload.get("Record_ID"))
    if not ats_record_id:
        logger.error("No ATS record id in payload")
        return jsonify({"status": "missing_ats_id"}), 400

    candidate_id = (payload.get("Candidate_ID") or payload.get("candidate_id") or payload.get("candidateId"))
    candidate_email = (payload.get("candidate_email") or payload.get("Candidate_Email") or payload.get("email"))
    attachment_ids = parse_attachment_ids(payload)

    # If no attachments provided - fetch ATS to find email+attachments
    if not attachment_ids:
        logger.info(f"Fetching ATS record {ats_record_id} to extract email and attachments")
        try:
            fetched_email, fetched_attachments = get_email_and_attachments_from_ats(ats_record_id)
            logger.info(f"Fetched email from ATS: {fetched_email}")
            logger.info(f"Fetched attachments from ATS: {fetched_attachments}")
            if fetched_email and not candidate_email:
                candidate_email = fetched_email
            if fetched_attachments:
                attachment_ids = fetched_attachments
        except Exception as e:
            logger.exception(f"Error reading ATS {ats_record_id}: {e}")
            append_received_log({"list_attachments_error_for": ats_record_id, "error": str(e)})
            return jsonify({"status": "error_listing_attachments", "ats_record_id": ats_record_id}), 500

    if not attachment_ids:
        logger.info(f"No attachments found for ATS {ats_record_id}")
        return jsonify({"status": "no_attachments", "ats_record_id": ats_record_id, "email": candidate_email}), 200

    # Find candidate by email if candidate_id not provided
    if not candidate_id and candidate_email:
        logger.info(f"Searching candidate by email: {candidate_email}")
        candidate_id = search_candidate_by_email(candidate_email)
        if candidate_id:
            logger.info(f"Candidate found: {candidate_id}")
        else:
            logger.error(f"No candidate found with email {candidate_email}")
            # try to create candidate
            try:
                created_cid = create_candidate_from_ats(ats_record_id)
                if created_cid:
                    candidate_id = created_cid
                    logger.info(f"Candidate created from ATS: {candidate_id}")
            except Exception:
                pass

    # Download attachments to ATTACH_DIR and then upload to candidate if found
    results = []
    for aid in attachment_ids:
        try:
            logger.info(f"Downloading attachment {aid} to disk")
            saved_path, saved_name = download_file_from_ats_to_disk(aid, ATTACH_DIR)
            logger.info(f"Saved attachment to {saved_path}")
            upload_result = None
            if candidate_id:
                logger.info(f"Uploading {saved_name} to candidate {candidate_id}")
                status_code, resp = upload_file_to_candidate(candidate_id, saved_path, saved_name, category="Resume")
                upload_result = {"status_code": status_code, "response": resp}
                logger.info(f"Upload result for {saved_name}: {status_code}")
            results.append({
                "attachment_id": aid,
                "saved_path": str(saved_path),
                "saved_name": saved_name,
                "upload": upload_result
            })
        except Exception as e:
            logger.exception(f"Error processing attachment {aid}: {e}")
            results.append({"attachment_id": aid, "error": str(e)})

    return jsonify({
        "status": "done",
        "ats_record_id": ats_record_id,
        "candidate_id": candidate_id,
        "candidate_email": candidate_email,
        "results": results
    }), 200


@app.route("/debug_ats/<ats_id>", methods=["GET"])
def debug_ats(ats_id):
    try:
        record = fetch_ats_record(ats_id)
    except Exception as e:
        logger.exception("debug_ats fetch failed")
        return jsonify({"error": str(e), "exists": False, "record_id": ats_id}), 500
    if not record:
        return jsonify({"error": "Record not found with any endpoint", "exists": False, "record_id": ats_id}), 200

    # compact preview
    preview = {}
    for k, v in list(record.items())[:40]:
        if isinstance(v, (str, int, float, bool)) or v is None:
            preview[k] = v
        elif isinstance(v, dict):
            preview[k] = {"type": "dict", "keys": list(v.keys())[:10]}
        elif isinstance(v, list):
            preview[k] = {"type": "list", "len": len(v), "sample": v[:2]}
        else:
            preview[k] = {"type": type(v).__name__}
    email, attachments = get_email_and_attachments_from_ats(ats_id)
    return jsonify({
        "exists": True,
        "record_id": ats_id,
        "email": email,
        "attachment_ids": attachments,
        "record_preview": preview
    }), 200


# ---------------------------
# CLI helpers
# ---------------------------
def check_ats_and_print(ats_id: str) -> int:
    try:
        logger.info(f"CLI check_ats for ATS id: {ats_id}")
        email, attachments = get_email_and_attachments_from_ats(ats_id)
        if email:
            print(f"Email found: {email}")
        else:
            print("Email: NOT FOUND")
        if attachments:
            print(f"Attachments FOUND: {len(attachments)}")
            for a in attachments:
                print(f" - {a}")
            found = True
        else:
            print("Attachments: NOT FOUND")
            found = False
        summary = {
            "ats_record_id": ats_id,
            "email": email,
            "found_attachments": found,
            "attachment_ids": attachments,
            "ts": current_timestamp()
        }
        print("\nJSON Summary:")
        print(json.dumps(summary, ensure_ascii=False, indent=2))
        return 0
    except Exception as e:
        logger.exception(f"check_ats_and_print failed for {ats_id}: {e}")
        print("ERROR:", str(e))
        return 1


def download_attachment_cli(att_id: str) -> int:
    try:
        logger.info(f"CLI download_attachment for ID: {att_id}")
        path, name = download_file_from_ats_to_disk(att_id, ATTACH_DIR)
        print(f"Saved to: {path}")
        return 0
    except Exception as e:
        logger.exception("download_attachment_cli failed")
        print("ERROR:", e)
        return 2


# ---------------------------
# Run (dev + CLI)
# ---------------------------
def main():
    ap = argparse.ArgumentParser(description="Zoho webhook receiver / helpers")
    ap.add_argument("--port", type=int, default=int(os.environ.get("WEBHOOK_PORT", "5057")), help="port to run Flask app")
    ap.add_argument("cmd", nargs="?", help="optional command: get_token_only | check_ats | create_candidate_from_ats | download_attachment")
    ap.add_argument("arg", nargs="?", help="argument for command (e.g. ATS id)")
    args = ap.parse_args()

    if args.cmd:
        cmd = args.cmd.lower()
        if cmd == "get_token_only":
            try:
                print(get_access_token())
                return 0
            except Exception as e:
                print("ERROR:", e)
                return 2
        if cmd == "check_ats":
            if not args.arg:
                print("Usage: check_ats <ATS_ID>")
                return 2
            return check_ats_and_print(args.arg)
        if cmd == "create_candidate_from_ats":
            if not args.arg:
                print("Usage: create_candidate_from_ats <ATS_ID>")
                return 2
            cid = create_candidate_from_ats(args.arg)
            if cid:
                print("Candidate ID:", cid)
                return 0
            else:
                print("Failed to create candidate")
                return 1
        if cmd == "download_attachment":
            if not args.arg:
                print("Usage: download_attachment <ATTACH_ID>")
                return 2
            return download_attachment_cli(args.arg)
        print("Unknown command:", args.cmd)
        return 2

    # No command -> run Flask app
    try:
        ATTACH_DIR.chmod(0o770)
    except Exception:
        pass
    logger.info(f"Starting Flask server on port {args.port}")
    app.run(host="0.0.0.0", port=args.port)


if __name__ == "__main__":
    raise SystemExit(main())
