# copytocandidates.py
import requests
import json
from pathlib import Path
import os
import sys
import io
import traceback
from datetime import datetime
from typing import Optional, List

# ---------------------------
# Config
# ---------------------------
CLIENT_ID = "1000.ZNQ17ZU1F48Z6DTTBKPRS8HYYGPU2C"
CLIENT_SECRET = "31128c41fe165770eabec0110a9f121b8a9e0846d9"
AUTH_CODE = ""  # optional
REDIRECT_URI = "https://www.zoho.com"

TOKENS_FILE = Path("/var/www/zoho/tokens.json")
RECRUIT_API_BASE = "https://recruit.zoho.com/recruit/v2"
OUTPUT_FOLDER = Path("/var/www/zoho/attache")
OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)

# Logging
LOG_DIR = Path("/var/www/zoho/Log")
LOG_DIR.mkdir(parents=True, exist_ok=True)
LOG_RECEIVED = LOG_DIR / "received.log"
LOG_SUCCESS = LOG_DIR / "success.log"
LOG_FAILED = LOG_DIR / "failed.log"
LOG_ERROR = LOG_DIR / "error.log"
LOG_FILES = LOG_DIR / "files.log"

ATTACHMENTS_CATEGORY = "Resume"

# ---------------------------
# Logging helpers
# ---------------------------
def _now():
    return datetime.utcnow().isoformat() + "Z"

def _append_json_log(path: Path, obj: dict):
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "a", encoding="utf-8") as f:
            f.write(json.dumps(obj, ensure_ascii=False) + "\n")
    except Exception as e:
        print("[LOG_ERROR] Failed to write log:", path, e)

def log_received(atsid: str, raw: Optional[dict] = None):
    entry = {"timestamp": _now(), "atsid": atsid}
    if raw is not None:
        entry["raw"] = raw
    _append_json_log(LOG_RECEIVED, entry)
    print(f"[LOG RECEIVED] {atsid}")

def log_success(message: str, meta: Optional[dict] = None):
    entry = {"timestamp": _now(), "message": message}
    if meta:
        entry["meta"] = meta
    _append_json_log(LOG_SUCCESS, entry)
    print(f"[LOG SUCCESS] {message}")

def log_failed(message: str, meta: Optional[dict] = None):
    entry = {"timestamp": _now(), "message": message}
    if meta:
        entry["meta"] = meta
    _append_json_log(LOG_FAILED, entry)
    print(f"[LOG FAILED] {message}")

def log_error(message: str, exc: Optional[Exception] = None, meta: Optional[dict] = None):
    entry = {"timestamp": _now(), "message": message}
    if exc:
        entry["exception"] = repr(exc)
        entry["traceback"] = traceback.format_exc()
    if meta:
        entry["meta"] = meta
    _append_json_log(LOG_ERROR, entry)
    print(f"[LOG ERROR] {message}")
    if exc:
        print(traceback.format_exc())

def log_file_status(file_name: str, status: str, candidate_id: Optional[str] = None, reason: Optional[str] = None):
    entry = {"timestamp": _now(), "file_name": file_name, "status": status}
    if candidate_id:
        entry["candidate_id"] = candidate_id
    if reason:
        entry["reason"] = reason
    _append_json_log(LOG_FILES, entry)
    print(f"[LOG FILE] {file_name} -> {status}")

# ---------------------------
# Token handling (lazy)
# ---------------------------
def load_tokens() -> dict:
    try:
        if TOKENS_FILE.exists():
            with open(TOKENS_FILE, "r", encoding="utf-8") as f:
                return json.load(f)
    except Exception as e:
        log_error("Failed to load tokens.json", exc=e, meta={"path": str(TOKENS_FILE)})
    return {}

def save_tokens(tokens: dict):
    try:
        TOKENS_FILE.parent.mkdir(parents=True, exist_ok=True)
        with open(TOKENS_FILE, "w", encoding="utf-8") as f:
            json.dump(tokens, f, indent=4)
    except Exception as e:
        log_error("Failed to save tokens.json", exc=e, meta={"path": str(TOKENS_FILE)})

def refresh_access_token(refresh_token: str) -> Optional[str]:
    url = "https://accounts.zoho.com/oauth/v2/token"
    params = {
        "refresh_token": refresh_token,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "grant_type": "refresh_token"
    }
    try:
        resp = requests.post(url, params=params, timeout=30)
    except Exception as e:
        log_error("HTTP error when refreshing token", exc=e, meta={"url": url})
        return None

    if resp.status_code == 200:
        new = resp.json()
        tokens = load_tokens()
        tokens["access_token"] = new.get("access_token")
        if "refresh_token" in new:
            tokens["refresh_token"] = new["refresh_token"]
        save_tokens(tokens)
        log_success("Access token refreshed", {"token": True})
        return tokens.get("access_token")
    else:
        log_failed("Failed to refresh token", {"status_code": resp.status_code, "text": resp.text})
        return None

def get_tokens_with_auth_code(auth_code: str) -> Optional[dict]:
    url = "https://accounts.zoho.com/oauth/v2/token"
    params = {
        "code": auth_code,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "redirect_uri": REDIRECT_URI,
        "grant_type": "authorization_code"
    }
    try:
        resp = requests.post(url, params=params, timeout=30)
    except Exception as e:
        log_error("HTTP error when exchanging AUTH_CODE", exc=e, meta={"url": url})
        return None

    if resp.status_code == 200:
        tokens = resp.json()
        save_tokens(tokens)
        log_success("Obtained tokens via AUTH_CODE")
        return tokens
    else:
        log_failed("Failed to exchange AUTH_CODE", {"status_code": resp.status_code, "text": resp.text})
        return None

def get_valid_access_token() -> Optional[str]:
    tokens = load_tokens()
    if "access_token" in tokens:
        return tokens["access_token"]
    if "refresh_token" in tokens:
        return refresh_access_token(tokens["refresh_token"])
    if AUTH_CODE:
        tokens = get_tokens_with_auth_code(AUTH_CODE)
        if tokens:
            return tokens.get("access_token")
    log_error("No valid token available (access/refresh/AUTH_CODE missing)")
    return None

# ---------------------------
# HTTP helpers
# ---------------------------
def auth_headers() -> dict:
    token = get_valid_access_token()
    if not token:
        raise RuntimeError("No valid Zoho access token available")
    return {"Authorization": f"Zoho-oauthtoken {token}"}

def api_request(method: str, url: str, **kwargs) -> requests.Response:
    headers = kwargs.pop("headers", {})
    headers.update(auth_headers())
    try:
        response = requests.request(method, url, headers=headers, **kwargs)
    except Exception as e:
        log_error("HTTP request failed", exc=e, meta={"method": method, "url": url})
        raise
    if response.status_code == 401:
        log_error("Access token expired/unauthorized; trying refresh", meta={"url": url})
        tokens = load_tokens()
        if "refresh_token" in tokens:
            new_token = refresh_access_token(tokens["refresh_token"])
            if new_token:
                headers.update({"Authorization": f"Zoho-oauthtoken {new_token}"})
                response = requests.request(method, url, headers=headers, **kwargs)
    return response

# ---------------------------
# ATS / Attachments helpers
# ---------------------------
def get_ats_record(ats_id: str) -> Optional[dict]:
    url = f"{RECRUIT_API_BASE}/ATS/{ats_id}"
    try:
        resp = api_request("GET", url, timeout=30)
    except Exception as e:
        log_error("Failed to call get_ats_record", exc=e, meta={"atsid": ats_id, "url": url})
        return None

    if resp.status_code == 200:
        data = resp.json().get("data")
        if data:
            log_success("Fetched ATS record", {"atsid": ats_id})
            return data[0]
    log_failed("Failed to get ATS record", {"atsid": ats_id, "status_text": resp.text})
    return None

def get_attachments(module: str, record_id: str) -> list:
    url = f"{RECRUIT_API_BASE}/{module}/{record_id}/Attachments"
    try:
        resp = api_request("GET", url, timeout=30)
    except Exception as e:
        log_error("Failed to call get_attachments", exc=e, meta={"module": module, "record_id": record_id, "url": url})
        return []

    if resp.status_code == 200:
        arr = resp.json().get("data", [])
        log_success("Fetched attachments list", {"module": module, "record_id": record_id, "count": len(arr)})
        return arr
    log_failed("Failed to fetch attachments", {"module": module, "record_id": record_id, "status_text": resp.text})
    return []

def download_attachment(file_obj: dict, module: str, record_id: str, folder_path: Path) -> Optional[Path]:
    file_name = file_obj.get("File_Name") or file_obj.get("file_name")
    file_id = file_obj.get("id")
    if not file_name or not file_id:
        log_failed("Attachment missing file_name or id", {"file_obj": file_obj, "module": module, "record_id": record_id})
        return None

    url = f"{RECRUIT_API_BASE}/{module}/{record_id}/Attachments/{file_id}"
    try:
        resp = api_request("GET", url, stream=True, timeout=60)
    except Exception as e:
        log_error("Failed to download attachment (request error)", exc=e, meta={"file_name": file_name, "record_id": record_id})
        log_file_status(file_name, "download_failed", reason=str(e))
        return None

    if resp.status_code == 200:
        folder_path.mkdir(parents=True, exist_ok=True)
        file_path = folder_path / file_name
        try:
            with open(file_path, "wb") as fh:
                for chunk in resp.iter_content(chunk_size=8192):
                    if chunk:
                        fh.write(chunk)
            log_success("Downloaded attachment", {"file": file_name, "record_id": record_id})
            log_file_status(file_name, "downloaded")
            return file_path
        except Exception as e:
            log_error("Failed to write downloaded file to disk", exc=e, meta={"file": str(file_path)})
            log_file_status(file_name, "download_failed", reason=str(e))
            return None
    else:
        log_failed("Attachment download failed (non-200)", {"file": file_name, "status": resp.status_code, "text": resp.text})
        log_file_status(file_name, "download_failed", reason=resp.text)
        return None

# ---------------------------
# Candidate creation & upload
# ---------------------------
def create_candidate_from_ats(ats_record: dict) -> Optional[str]:
    url = f"{RECRUIT_API_BASE}/Candidates"
    last_name = ats_record.get("Last_Name") or ats_record.get("Last_name") or ats_record.get("CustomModule4_Name") or "Unknown"
    first_name = ats_record.get("First_Name") or ats_record.get("CustomModule4_Name") or None

    candidate_payload = {
        "data": [
            {
                "First_Name": first_name,
                "Last_Name": last_name,
                "Email": ats_record.get("Email") or ats_record.get("Profile_urls") or "",
                "Mobile": ats_record.get("Mobile_No") or ats_record.get("Mobile") or "",
                "Phone": ats_record.get("Phone_No") or ats_record.get("Phone") or "",
                "City": ats_record.get("City") or "",
                "Country": ats_record.get("Country") or "",
            }
        ]
    }

    try:
        resp = api_request("POST", url, json=candidate_payload, timeout=30)
    except Exception as e:
        log_error("Failed to create candidate (request error)", exc=e, meta={"payload": candidate_payload})
        return None

    if resp.status_code in (200, 201):
        try:
            data = resp.json().get("data", [])
            if data and isinstance(data, list):
                details = data[0].get("details") or {}
                candidate_id = details.get("id") or details.get("name") or None
                if candidate_id:
                    log_success("Candidate created", {"candidate_id": candidate_id, "last_name": last_name})
                    return str(candidate_id)
        except Exception as e:
            log_error("Failed to parse create candidate response", exc=e, meta={"text": resp.text})
            return None

    log_failed("Failed to create candidate", {"status_code": resp.status_code, "text": resp.text})
    return None

def upload_attachment_to_candidate(file_path: Path, candidate_id: str, category: str = ATTACHMENTS_CATEGORY) -> bool:
    url = f"{RECRUIT_API_BASE}/Candidates/{candidate_id}/Attachments"

    try:
        with open(file_path, "rb") as fh:
            content = fh.read()
    except Exception as e:
        log_error("Failed to open file for upload", exc=e, meta={"file": str(file_path)})
        log_file_status(file_path.name, "upload_failed", candidate_id=candidate_id, reason=str(e))
        return False

    file_tuple = (file_path.name, io.BytesIO(content))
    files = {"file": file_tuple}
    data = {"attachments_category": category}

    try:
        resp = api_request("POST", url, files=files, data=data, timeout=90)
    except Exception as e:
        log_error("Upload request failed", exc=e, meta={"file": file_path.name, "candidate_id": candidate_id})
        log_file_status(file_path.name, "upload_failed", candidate_id=candidate_id, reason=str(e))
        return False

    if resp.status_code in (200, 201):
        log_success("Uploaded file to candidate", {"file": file_path.name, "candidate_id": candidate_id, "category": category})
        log_file_status(file_path.name, "uploaded", candidate_id=candidate_id)
        return True
    else:
        try:
            txt = resp.text
            j = resp.json()
        except Exception:
            txt = resp.text
            j = None
        log_failed("Failed to upload file to candidate", {"file": file_path.name, "candidate_id": candidate_id, "status": resp.status_code, "response": j or txt})
        log_file_status(file_path.name, "upload_failed", candidate_id=candidate_id, reason=txt)
        return False

# ---------------------------
# process one ATS (callable)
# ---------------------------
def process_one_ats(ats_id: str, raw: Optional[dict] = None) -> bool:
    start = datetime.utcnow()
    log_received(ats_id, raw=raw)
    try:
        record = get_ats_record(ats_id)
        if not record:
            log_failed("No ATS record returned", {"atsid": ats_id})
            return False

        record_folder = OUTPUT_FOLDER / ats_id
        record_folder.mkdir(parents=True, exist_ok=True)
        json_file = record_folder / f"{ats_id}.json"
        with open(json_file, "w", encoding="utf-8") as jf:
            json.dump(record, jf, indent=4, ensure_ascii=False)
        log_success("Saved ATS JSON", {"atsid": ats_id, "path": str(json_file)})

        attachments = get_attachments("ATS", ats_id)
        downloaded_files = []
        if attachments:
            for att in attachments:
                downloaded = download_attachment(att, "ATS", ats_id, record_folder)
                if downloaded:
                    downloaded_files.append(downloaded)
        else:
            log_success("No attachments for ATS", {"atsid": ats_id})

        candidate_id = create_candidate_from_ats(record)
        if candidate_id:
            for idx, file_path in enumerate(downloaded_files):
                category = "Resume" if idx == 0 else "Others"
                ok = upload_attachment_to_candidate(file_path, candidate_id, category=category)
                if not ok:
                    log_failed("One file failed to upload", {"file": str(file_path), "candidate_id": candidate_id})
            duration = (datetime.utcnow() - start).total_seconds()
            log_success("Process completed", {"atsid": ats_id, "candidate_id": candidate_id, "duration_seconds": duration})
            return True

        log_failed("Candidate not created; skipping uploads", {"atsid": ats_id})
        return False

    except Exception as e:
        log_error("Unexpected exception in process_one_ats", exc=e, meta={"atsid": ats_id})
        return False

# ---------------------------
# Webhook handler
# ---------------------------
def process_webhook_data(webhook_data: dict) -> List[str]:
    """
    Process webhook data and extract ATS IDs
    Modify this function based on your webhook structure
    """
    processed_ids = []
    
    # Example: extract from webhook data
    # Adjust this based on your actual webhook structure
    if 'data' in webhook_data:
        for item in webhook_data['data']:
            if 'id' in item:
                processed_ids.append(str(item['id']))
    
    return processed_ids

# ---------------------------
# Main execution
# ---------------------------
if __name__ == "__main__":
    import argparse
    
    parser = argparse.ArgumentParser(description='Process ATS records from Zoho Recruit')
    parser.add_argument('--ids', nargs='+', help='List of ATS IDs to process')
    parser.add_argument('--webhook-file', help='Path to webhook data JSON file')
    
    args = parser.parse_args()
    
    ats_ids_to_process = []
    
    # Case 1: Process specific IDs from command line
    if args.ids:
        ats_ids_to_process.extend(args.ids)
        print(f"Processing {len(ats_ids_to_process)} ATS IDs from command line")
    
    # Case 2: Process IDs from webhook file
    elif args.webhook_file:
        try:
            with open(args.webhook_file, 'r', encoding='utf-8') as f:
                webhook_data = json.load(f)
            ats_ids_to_process = process_webhook_data(webhook_data)
            print(f"Processing {len(ats_ids_to_process)} ATS IDs from webhook file")
        except Exception as e:
            log_error("Failed to process webhook file", exc=e)
            sys.exit(1)
    
    # Case 3: No IDs provided - wait for webhook input or exit
    else:
        print("No ATS IDs provided. Waiting for webhook input...")
        print("You can either:")
        print("1. Run with --ids ID1 ID2 ID3")
        print("2. Run with --webhook-file path/to/webhook.json")
        print("3. Set up as webhook endpoint and send POST data")
        sys.exit(0)
    
    # Process all collected ATS IDs
    success_count = 0
    for ats_id in ats_ids_to_process:
        if process_one_ats(ats_id):
            success_count += 1
    
    print(f"Processing completed: {success_count}/{len(ats_ids_to_process)} successful")