#!/usr/bin/env python3
"""Pixelixe backend starter (Python 3, standard library only).
Example: PIXELIXE_API_KEY=... python3 automation-batch.py \
  --template certificate-template.json --csv certificate-example.csv --format pdf
Run on your server or computer. Never put a private API key in browser code.
Creates one saved template in your account, then one rendering request per row.
API usage is subject to your account plan. Check outputs before real distribution.
"""
import argparse, csv, json, os, pathlib, re, time, unicodedata
from urllib.request import Request, urlopen
from urllib.error import HTTPError
BASE = 'https://studio.pixelixe.com/api/'
def post(path, body, key):
    data = json.dumps(body).encode('utf-8')
    for attempt in range(3):
        try:
            req = Request(BASE + path, data=data, headers={
                'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json'}, method='POST')
            with urlopen(req, timeout=120) as response:
                return response.read(), response.headers.get_content_type()
        except HTTPError as error:
            if error.code in (429, 500, 502, 503, 504) and attempt < 2:
                time.sleep(2 ** (attempt + 1)); continue
            raise RuntimeError('Pixelixe returned HTTP ' + str(error.code) + '. Check your account and request.') from None
    raise RuntimeError('Request failed.')
def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument('--template', required=True); p.add_argument('--csv', required=True)
    p.add_argument('--format', choices=['png', 'pdf'], default='png')
    p.add_argument('--output', default='generated'); p.add_argument('--limit', type=int, default=100)
    a = p.parse_args(); key = os.environ.get('PIXELIXE_API_KEY', '').strip()
    if not key: p.error('Set PIXELIXE_API_KEY in your server environment.')
    scene = json.loads(pathlib.Path(a.template).read_text(encoding='utf-8'))
    scene['save_document'] = True
    text_fields = {m['name'] for m in scene['modifications'] if m['type'] == 'text'}
    with open(a.csv, encoding='utf-8-sig', newline='') as source:
        reader = csv.DictReader(source)
        headers = reader.fieldnames or []
        required = ['recipient_name','course_title','issue_date','issuer_name'] if 'recipient_name' in text_fields else ['quote','reviewer_name','reviewer_role','brand_name','rating']
        if not set(required).issubset(headers): p.error('Missing required CSV headers: '+', '.join(required))
        rows = list(reader)
    if not 1 <= len(rows) <= a.limit: p.error('CSV must contain 1 to --limit data rows.')
    for i, row in enumerate(rows, 1):
        if None in row or any(not (row.get(k) or '').strip() for k in required): p.error('Invalid or empty field in CSV row '+str(i+1))
        if 'rating' in required and row['rating'].strip() not in ['1','2','3','4','5']: p.error('Rating must be an integer from 1 to 5.')
    out = pathlib.Path(a.output); out.mkdir(parents=True, exist_ok=True)
    if any(out.iterdir()): p.error('Choose an empty output directory to avoid overwriting previous files.')
    raw, mime = post('jsontographic/v2', scene, key)
    response = json.loads(raw)
    uid = response.get('document_uid')
    if not uid: raise RuntimeError('Template was not saved. Check your request and account.')
    manifest = {'document_uid':uid, 'outputs':[]}
    (out/'manifest.json').write_text(json.dumps(manifest, indent=2), encoding='utf-8')
    for i, row in enumerate(rows, 1):
        mods = []
        for name in required:
            value = row[name].strip()
            if name == 'rating': value += ' / 5'
            change = {'element_name': name, 'type':'text', 'text':value}
            if name == 'recipient_name' and len(value)>24: change['font-size']='60px'
            mods.append(change)
        body = {'document_uid':uid, 'format':'pdf' if a.format=='pdf' else 'image', 'image_type':'png', 'modifications':mods}
        content, mime = post('graphic/automation/v2', body, key)
        if not (content.startswith(b'%PDF-') if a.format=='pdf' else content.startswith(b'\x89PNG\r\n\x1a\n')):
            raise RuntimeError('Unexpected render response for row '+str(i)+'. No file was written for this row.')
        person = row.get('recipient_name') or row.get('reviewer_name') or str(i)
        slug = re.sub(r'[^A-Za-z0-9_-]+','-',unicodedata.normalize('NFKD',person).encode('ascii','ignore').decode()).strip('-')[:60] or 'image'
        filename = f'{i:04d}-{slug}.{a.format}'
        (out/filename).write_bytes(content)
        manifest['outputs'].append({'row':i,'file':filename})
        (out/'manifest.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8')
        print(f'{i}/{len(rows)}: {filename}')
    print('Done. Review every output before sending or publishing it.')
if __name__ == '__main__': main()
