#!/usr/bin/env python3
"""Your first useful program on Extella: a report over a CSV file on your own machine.

    python3 first_program.py                       # sample data, default parameters
    python3 first_program.py --group_by product --top 3
    python3 first_program.py --path ~/my/sales.csv --group_by customer --value total

What it does, in the open:
  1. downloads two programs from extella.dev: csv_report and csv_report_sample (plain text, read them first if you like),
     prints their sha256 and stops if they differ from the versions this script was written for
  2. saves both into your Extella account. PROGRAMS WITH THESE TWO NAMES IN YOUR ACCOUNT ARE OVERWRITTEN; the script asks before saving
  3. without --path: runs csv_report_sample on your device, which writes ~/extella-demo-data/orders.csv (synthetic orders)
  4. runs csv_report on your device with your parameters and prints the table

Needs: Extella installed and signed in (it writes ~/.extella/api_token.txt and ~/.extella/device.txt). Python 3.8+, standard library only.
The programs run on your machine with your user's rights; there is no sandbox. Options --token, --device, --agent override the files; the token can also be given in the
EXTELLA_TOKEN environment variable, which keeps it out of the shell history.
"""
import argparse
import ast
import hashlib
import json
import os
import sys
import time
import urllib.error
import urllib.request

API, SITE = "https://api.extella.ai/api", "https://extella.dev/report/"
HOME = os.path.join(os.path.expanduser("~"), ".extella")
SAMPLE_PATH = "~/extella-demo-data/orders.csv"
DESCRIPTIONS = {"csv_report": "Report over a CSV file on the device: group rows by one column, sum a numeric column, return the top groups. Parameters: path, group_by, value, top, filter_column, filter_value.",
                "csv_report_sample": "Writes a file of synthetic orders (CSV) on the device it runs on."}
PINNED = {"csv_report": "403f4b99fdbfdc5f16dccd76cfeb736a023c57a7f8f3e862a25218153db92723",          # sha256 of the two programs this script was written for
          "csv_report_sample": "9f28576d1dd5f6a64dca61b9e55b1b0f57851c2b6e7e38139228c7af73a611df"}


def read(path):
    try:
        with open(path, encoding="utf-8") as f:
            return f.read().strip()
    except OSError:
        return ""


ap = argparse.ArgumentParser(description="Save and run a CSV report program on your own device through Extella.")
ap.add_argument("--path", default="", help="a CSV file on your device (default: a sample file is created)")
ap.add_argument("--group_by", default="region"); ap.add_argument("--value", default="amount"); ap.add_argument("--top", type=int, default=5)
ap.add_argument("--filter_column", default=""); ap.add_argument("--filter_value", default="")
ap.add_argument("--token", default=""); ap.add_argument("--device", default=""); ap.add_argument("--agent", default="agent_extella_default")
ap.add_argument("--yes", action="store_true", help="do not ask before saving the two programs")
ap.add_argument("--accept-changed", action="store_true", help="save the programs even if they differ from the versions this script was written for")
ap.add_argument("--site", default=SITE, help=argparse.SUPPRESS)
a = ap.parse_args()
token, device = a.token or os.environ.get("EXTELLA_TOKEN", "") or read(os.path.join(HOME, "api_token.txt")), a.device or read(os.path.join(HOME, "device.txt"))
if not token or not device:
    sys.exit("Extella is not signed in on this machine: ~/.extella/api_token.txt or ~/.extella/device.txt is missing.\nInstall Extella from https://extella.dev/#download, sign in, then run this script again.")


def call(path, body, timeout=150):
    req = urllib.request.Request(API + path, data=json.dumps(body).encode(), headers={"X-Auth-Token": token, "X-Profile-Id": "default", "X-Agent-Id": a.agent, "Content-Type": "application/json"})
    try:
        return json.loads(urllib.request.urlopen(req, timeout=timeout).read())
    except urllib.error.HTTPError as e:
        return {"status": "error", "reason": "HTTP %d: %s" % (e.code, e.read().decode("utf-8", "ignore")[:200])}
    except Exception as e:
        return {"status": "error", "reason": "no answer from the platform: " + str(e)[:120]}


def run(name, params):
    t0 = time.time(); out = call("/expert/run", {"name": name, "params": params, "targets": [device]})
    while isinstance(out, dict) and out.get("task_id") and "result" not in out:      # long calls come back deferred
        time.sleep(2); chk = call("/tasks/check", {"task_id": out["task_id"]}, timeout=30)
        out = chk if str(chk.get("status")) in ("completed", "done", "success", "finished", "error", "failed") else {"task_id": out["task_id"]}
    res = out.get("result", out) if isinstance(out, dict) else out
    if isinstance(res, dict) and "result" in res: res = res["result"]
    if isinstance(res, str):
        for loader in (json.loads, ast.literal_eval):
            try: res = loader(res); break
            except Exception: pass
    return (res if isinstance(res, dict) else {"status": "error", "reason": str(res)[:300]}), round(time.time() - t0, 1)


print("1/4  downloading the two programs from %s ..." % a.site, end=" ", flush=True)
try:
    source = {n: urllib.request.urlopen(a.site + n + ".fython", timeout=30).read() for n in DESCRIPTIONS}
except Exception as e:
    sys.exit("failed: " + str(e)[:120])
print("ok")
changed = [n for n in source if hashlib.sha256(source[n]).hexdigest() != PINNED[n]]
for n in source:
    print("     %s%s.fython  sha256 %s%s" % (a.site, n, hashlib.sha256(source[n]).hexdigest(), "  (differs from the version this script was written for)" if n in changed else ""))
if changed and not a.accept_changed:
    sys.exit("stopped: %s on the site differ%s from the version this script was written for; nothing was saved.\nDownload first_program.py again, or read the new text and pass --accept-changed." % (" and ".join(changed), "s" if len(changed) == 1 else ""))
source = {n: text.decode("utf-8") for n, text in source.items()}
if not a.yes:
    try:
        answer = input("2/4  save csv_report and csv_report_sample into your account (agent %s)? Programs with these names will be overwritten. [y/N] " % a.agent)
    except EOFError:
        sys.exit("\nno terminal to ask on: nothing was saved. Run the script in a terminal, or pass --yes.")
    if answer.strip().lower() not in ("y", "yes"):
        sys.exit("nothing was saved")
for n in source:
    r = call("/expert/save", {"name": n, "description": DESCRIPTIONS[n], "code": source[n], "cspl": "fython", "kwargs": {}, "global": False})
    if r.get("status") != "success":
        sys.exit("could not save %s: %s" % (n, r.get("reason") or r))
print("2/4  saved both programs")
path = a.path or SAMPLE_PATH
if not a.path:
    r, s = run("csv_report_sample", {"path": SAMPLE_PATH})
    if r.get("status") != "success":
        sys.exit("3/4  could not write the sample file: %s" % (r.get("reason") or r))
    print("3/4  sample file written on your device in %.1fs: %s, %s rows" % (s, r.get("file"), r.get("rows")))
else:
    print("3/4  using your file: " + path)
params = {"path": path, "group_by": a.group_by, "value": a.value, "top": a.top}
if a.filter_column:
    params.update({"filter_column": a.filter_column, "filter_value": a.filter_value})
r, s = run("csv_report", params)
if r.get("status") != "success":
    sys.exit("4/4  the report failed: %s%s" % (r.get("reason") or r, ("\n     columns in the file: " + ", ".join(r["columns"])) if r.get("columns") else ""))
print("4/4  csv_report ran on %s (%s) in %.1fs; %s of %s rows used\n" % (r.get("host"), r.get("os"), s, r.get("rows_used"), r.get("rows_in_file")))
print("%-28s %14s %8s %7s" % (a.group_by, "sum of " + a.value, "rows", "share"))
for row in r.get("table", []):
    print("%-28s %14s %8s %6s%%" % (str(row["group"])[:28], row["total"], row["rows"], row["share"]))
print("""
The program now lives in your account under the name csv_report. Call it again with other parameters:
    python3 first_program.py --yes --group_by product --top 3
or on your own file:
    python3 first_program.py --yes --path ~/path/to/file.csv --group_by <column> --value <numeric column>
To let an agent call it, connect the Extella MCP server (streamable HTTP, https://api.extella.ai/mcp/) with the headers
X-Auth-Token, X-Profile-Id: default and X-Agent-Id: %s, then ask the agent to run the expert csv_report.""" % a.agent)
