"""Submit complete Factory workflows as portable Kubernetes Jobs."""
from __future__ import annotations
import argparse, hashlib, json, os, re, secrets, subprocess, tempfile, time
from pathlib import Path
from typing import Any, Sequence
from urllib.parse import urlsplit

APPROVED_IMAGE="ghcr.io/alexcodeplace/overdeck-agent-sandbox@sha256:4b682407c1312da46f2c0f1d863dead53dffc45e811d9406881decd8cf015c24"
APPROVED_REPOSITORY="https://github.com/alexcodeplace/overdeck.git"
DNS=re.compile(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")

def run(argv: Sequence[str], *, cwd: Path|None=None, env=None, input_data: str|None=None, check=True):
    result=subprocess.run(argv,cwd=cwd,env=env,input=input_data,text=True,capture_output=True,check=False)
    if check and result.returncode:
        raise RuntimeError(f"{argv[0]} exited {result.returncode}: {(result.stderr or result.stdout).strip()[-1200:]}")
    return result

def git(repo: Path,*args: str,env=None)->str:
    return run(["git",*args],cwd=repo,env=env).stdout.strip()

def workspace_snapshot(repo: Path)->tuple[str,str,str]:
    base=git(repo,"rev-parse","--verify","HEAD^{commit}")
    with tempfile.NamedTemporaryFile(prefix="factory-k8s-index-") as index:
        env=os.environ.copy(); env["GIT_INDEX_FILE"]=index.name; index.truncate(0)
        git(repo,"read-tree",base,env=env); git(repo,"add","-A",env=env)
        tree=git(repo,"write-tree",env=env)
    commit_env=os.environ.copy()
    commit_env.update({"GIT_AUTHOR_NAME":"Overdeck Factory","GIT_AUTHOR_EMAIL":"factory@overdeck.invalid","GIT_COMMITTER_NAME":"Overdeck Factory","GIT_COMMITTER_EMAIL":"factory@overdeck.invalid"})
    commit=git(repo,"commit-tree",tree,"-p",base,env=commit_env)
    return base,tree,commit

def origin(repo: Path)->str:
    url=git(repo,"remote","get-url","origin"); parsed=urlsplit(url)
    if url!=APPROVED_REPOSITORY or parsed.scheme!="https" or not parsed.hostname or parsed.username or parsed.password or any(c in url for c in "\r\n\0"):
        raise ValueError("Factory Kubernetes transport requires the approved credential-free HTTPS origin URL")
    return url

def label(value: str,name: str)->str:
    if len(value)>63 or not DNS.fullmatch(value): raise ValueError(f"{name} must be a Kubernetes DNS label")
    return value

def manifest(*,job_name: str,namespace: str,image: str,secret: str,service_account: str,repo_url: str,execution_ref: str,result_ref: str,invocation: Sequence[str],timeout: int,pull_secret: str="overdeck-ghcr-pull")->dict[str,Any]:
    for value,name in ((job_name,"job name"),(namespace,"namespace"),(secret,"secret"),(service_account,"service account"),(pull_secret,"pull secret")): label(value,name)
    if image!=APPROVED_IMAGE: raise ValueError("FACTORY_K8S_IMAGE must use the approved immutable GHCR digest")
    if repo_url!=APPROVED_REPOSITORY: raise ValueError("Factory Kubernetes transport requires the approved repository URL")
    if not re.fullmatch(r"refs/heads/factory-exec/[0-9a-f]{24}",execution_ref): raise ValueError("execution ref is invalid")
    if not re.fullmatch(r"refs/heads/factory-result/[0-9a-f]{24}",result_ref): raise ValueError("result ref is invalid")
    if not invocation or any(not isinstance(v,str) for v in invocation): raise ValueError("invocation must contain strings")
    invocation_json=json.dumps(list(invocation),separators=(",",":"))
    if len(invocation_json.encode())>131072: raise ValueError("Factory invocation exceeds the Kubernetes control-plane limit")
    if not 60<=timeout<=86400: raise ValueError("timeout must be between 60 and 86400 seconds")
    labels={"app.kubernetes.io/name":"overdeck-factory","overdeck.dev/factory-job":job_name}
    locked={"allowPrivilegeEscalation":False,"readOnlyRootFilesystem":True,"capabilities":{"drop":["ALL"]}}
    mounts=[{"name":"workspace","mountPath":"/workspace"},{"name":"home","mountPath":"/home/factory"},{"name":"tmp","mountPath":"/tmp"},{"name":"credentials","mountPath":"/home/factory/.netrc","subPath":"git-netrc","readOnly":True},{"name":"credentials","mountPath":"/home/factory/.pi/agent/auth.json","subPath":"pi-auth.json","readOnly":True},{"name":"credentials","mountPath":"/home/factory/.pi/agent/models.json","subPath":"pi-models.json","readOnly":True}]
    pod={"serviceAccountName":service_account,"automountServiceAccountToken":False,"restartPolicy":"Never","imagePullSecrets":[{"name":pull_secret}],"securityContext":{"runAsNonRoot":True,"runAsUser":1000,"runAsGroup":1000,"fsGroup":1000,"seccompProfile":{"type":"RuntimeDefault"}},"initContainers":[{"name":"prepare-home","image":image,"imagePullPolicy":"Always","command":["/bin/mkdir"],"args":["-p","/home/factory/.pi/agent"],"securityContext":locked,"volumeMounts":[{"name":"home","mountPath":"/home/factory"}]}],"containers":[{"name":"factory","image":image,"imagePullPolicy":"Always","command":["/usr/local/bin/factory-kubernetes-worker"],"env":[{"name":"FACTORY_REPO_URL","value":repo_url},{"name":"FACTORY_EXECUTION_REF","value":execution_ref},{"name":"FACTORY_RESULT_REF","value":result_ref},{"name":"FACTORY_INVOCATION_JSON","value":invocation_json},{"name":"FACTORY_DATA_DIR","value":"/home/factory/state/data"},{"name":"FACTORY_DB_PATH","value":"/home/factory/state/sssf.db"},{"name":"PI_AUTH_PATH","value":"/home/factory/.pi/agent/auth.json"},{"name":"PI_MODELS_PATH","value":"/home/factory/.pi/agent/models.json"}],"resources":{"requests":{"cpu":os.getenv("FACTORY_K8S_CPU_REQUEST","1"),"memory":os.getenv("FACTORY_K8S_MEMORY_REQUEST","2Gi")},"limits":{"cpu":os.getenv("FACTORY_K8S_CPU_LIMIT","4"),"memory":os.getenv("FACTORY_K8S_MEMORY_LIMIT","8Gi")}},"securityContext":locked,"volumeMounts":mounts}],"volumes":[{"name":"workspace","emptyDir":{}},{"name":"home","emptyDir":{}},{"name":"tmp","emptyDir":{}},{"name":"credentials","secret":{"secretName":secret,"defaultMode":288}}]}
    return {"apiVersion":"batch/v1","kind":"Job","metadata":{"name":job_name,"namespace":namespace,"labels":labels},"spec":{"backoffLimit":0,"activeDeadlineSeconds":timeout,"ttlSecondsAfterFinished":3600,"template":{"metadata":{"labels":labels},"spec":pod}}}

def kubectl(namespace: str,kubeconfig: str|None,*args: str)->list[str]:
    out=["kubectl"]
    if kubeconfig: out += ["--kubeconfig",kubeconfig]
    return out+["--namespace",namespace,*args]

def delete_remote_ref(repo: Path,ref: str)->str|None:
    probe=run(["git","ls-remote","--refs","origin",ref],cwd=repo,check=False)
    if probe.returncode: return f"could not inspect {ref}: {(probe.stderr or probe.stdout).strip()}"
    if not probe.stdout.strip(): return None
    deleted=run(["git","push","origin",f":{ref}"],cwd=repo,check=False)
    if deleted.returncode: return f"could not delete {ref}: {(deleted.stderr or deleted.stdout).strip()}"
    return None

def submit(repo: Path,invocation: Sequence[str])->Path:
    image=os.getenv("FACTORY_K8S_IMAGE",""); secret=os.getenv("FACTORY_K8S_SECRET","overdeck-factory-credentials"); namespace=os.getenv("FACTORY_K8S_NAMESPACE","overdeck-factory"); account=os.getenv("FACTORY_K8S_SERVICE_ACCOUNT","overdeck-factory-runner"); kubeconfig=os.getenv("FACTORY_K8S_KUBECONFIG") or None; timeout=int(os.getenv("FACTORY_K8S_TIMEOUT_SECONDS","7200"))
    repo_url=origin(repo); _,input_tree,input_commit=workspace_snapshot(repo); nonce=secrets.token_hex(12); execution_ref=f"refs/heads/factory-exec/{nonce}"; result_ref=f"refs/heads/factory-result/{nonce}"; name=f"factory-{nonce[:16]}"
    job=manifest(job_name=name,namespace=namespace,image=image,secret=secret,service_account=account,repo_url=repo_url,execution_ref=execution_ref,result_ref=result_ref,invocation=invocation,timeout=timeout,pull_secret=os.getenv("FACTORY_K8S_IMAGE_PULL_SECRET","overdeck-ghcr-pull"))
    output_root=Path(git(repo,"rev-parse","--path-format=absolute","--git-path","factory-kubernetes")); output=output_root/name; output.mkdir(parents=True,exist_ok=True)
    primary: BaseException|None=None; cleanup_errors: list[str]=[]; created=False; result_path: Path|None=None; recovery_result_ref=False
    try:
        git(repo,"push","origin",f"{input_commit}:{execution_ref}")
        run(kubectl(namespace,kubeconfig,"create","-f","-"),input_data=json.dumps(job)); created=True
        deadline=time.monotonic()+timeout+30
        while time.monotonic()<deadline:
            status=json.loads(run(kubectl(namespace,kubeconfig,"get","job",name,"-o","json")).stdout); state=status.get("status",{})
            if state.get("succeeded") or state.get("failed"): break
            time.sleep(2)
        else: raise TimeoutError(f"Kubernetes Job {name} did not reach terminal status")
        logs=run(kubectl(namespace,kubeconfig,"logs",f"job/{name}"),check=False).stdout; (output/"status.json").write_text(json.dumps(status,indent=2)+"\n"); (output/"job.log").write_text(logs)
        if status.get("status",{}).get("succeeded")!=1: raise RuntimeError(f"Kubernetes Job {name} failed; API output saved under {output}")
        git(repo,"fetch","--no-tags","origin",result_ref); result_commit=git(repo,"rev-parse","FETCH_HEAD^{commit}")
        if run(["git","merge-base","--is-ancestor",input_commit,result_commit],cwd=repo,check=False).returncode: raise RuntimeError("result ref does not descend from the submitted workspace")
        if workspace_snapshot(repo)[1]!=input_tree: raise RuntimeError("workspace changed while Kubernetes Job ran; result not applied")
        delta=run(["git","diff","--binary",input_commit,result_commit,"--"],cwd=repo).stdout
        if delta:
            run(["git","apply","--check","--binary","-"],cwd=repo,input_data=delta); run(["git","apply","--binary","-"],cwd=repo,input_data=delta)
        result_path=output
    except BaseException as exc:
        primary=exc
        recovery_result_ref=run(["git","ls-remote","--exit-code","--refs","origin",result_ref],cwd=repo,check=False).returncode==0
        (output/"recovery.json").write_text(json.dumps({"execution_ref":execution_ref,"result_ref":result_ref if recovery_result_ref else None,"error":str(exc)},indent=2)+"\n")
    finally:
        if created:
            deleted=run(kubectl(namespace,kubeconfig,"delete","job",name,"--ignore-not-found","--wait=false"),check=False)
            if deleted.returncode: cleanup_errors.append(f"could not delete Job {name}: {(deleted.stderr or deleted.stdout).strip()}")
        for ref in ((result_ref,execution_ref) if primary is None else (execution_ref,)):
            if error:=delete_remote_ref(repo,ref): cleanup_errors.append(error)
    if primary:
        if recovery_result_ref: primary.add_note(f"recovery result ref retained: {result_ref}")
        for error in cleanup_errors: primary.add_note(error)
        raise primary
    if cleanup_errors: raise RuntimeError("; ".join(cleanup_errors))
    assert result_path is not None
    return result_path

def main(argv=None)->int:
    parser=argparse.ArgumentParser(prog="factory kubernetes"); parser.add_argument("--repo",default="."); parser.add_argument("invocation",nargs=argparse.REMAINDER); args=parser.parse_args(argv)
    repo=Path(git(Path(args.repo).resolve(),"rev-parse","--show-toplevel"))
    if not args.invocation: parser.error("an ADW invocation is required")
    print(f"factory kubernetes: completed; API output: {submit(repo,args.invocation)}"); return 0
if __name__=="__main__": raise SystemExit(main())
