#!/usr/bin/env python3
"""
keychain.py  -  deterministic Keychain helper, Python counterpart to credential-store.sh.

Purpose
-------
The pipeline reads Keychain tokens from many places (Phase 0 setup, Phase 1
Jira fetch, Phase 6 push retry, channels, ...). Shell `security` calls work
fine but make error handling and structured output painful. This module is
the single deterministic Python entry point for those operations.

API
---
get(label, account=None) -> str | None
    Read a generic-password item by label. Returns None if missing.

set(label, value, account=None) -> None
    Store / overwrite a generic-password item.

delete(label, account=None) -> bool
    Remove the item. Returns True if deleted, False if it did not exist.

list_labels(filter_substring=None) -> list[str]
    Return Keychain item labels matching the optional substring filter.
    Used by setup discovery.

doctor() -> dict
    Probe backend availability. Returns
    {"platform": "macos|linux|unknown", "backend_ok": bool, "remediation": str|None}

CLI
---
    python3 keychain.py get <label> [--account USER]
    python3 keychain.py set <label> <value> [--account USER]
    python3 keychain.py delete <label> [--account USER]
    python3 keychain.py list [--match SUBSTRING]
    python3 keychain.py doctor

Exit codes
----------
0  success
1  item missing (get) / does not exist (delete)
2  backend unavailable (no `security` on macOS, no `secret-tool` on Linux)
3  usage error
4  unexpected backend error (stderr captured to stdout for diagnostics)

Behaviour
---------
- macOS: shells out to /usr/bin/security, parsing structured output.
- Linux: shells out to secret-tool (libsecret).
- Windows / unknown platforms: backend unavailable; `doctor` prints remediation.
- Never logs the secret value. `set` accepts the value via argv (CLI) or via
  stdin when the value is exactly `-` (preferred for scripts to avoid shell
  history leak).
- get returns the raw value to stdout with no trailing newline manipulation
  beyond stripping the OS-level final \\n that `security -w` appends.

This file has no external dependencies. Python 3.10+ stdlib only.
"""

from __future__ import annotations

import argparse
import json
import os
import platform
import shutil
import subprocess
import sys
from typing import Optional


def _platform() -> str:
    s = platform.system()
    if s == "Darwin":
        return "macos"
    if s == "Linux":
        return "linux"
    return "unknown"


def _account_default(explicit: Optional[str] = None) -> str:
    return explicit or os.environ.get("USER") or os.environ.get("LOGNAME") or "user"


def _run(argv: list[str], input_value: Optional[bytes] = None) -> tuple[int, bytes, bytes]:
    """Run a subprocess; return (rc, stdout_bytes, stderr_bytes). Never raises on non-zero."""
    proc = subprocess.run(
        argv,
        input=input_value,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
    )
    return proc.returncode, proc.stdout, proc.stderr


def doctor() -> dict:
    pf = _platform()
    if pf == "macos":
        ok = shutil.which("security") is not None
        return {
            "platform": pf,
            "backend_ok": ok,
            "remediation": None if ok else "macOS Keychain CLI missing  -  /usr/bin/security should ship with macOS; reinstall Command Line Tools.",
        }
    if pf == "linux":
        ok = shutil.which("secret-tool") is not None
        return {
            "platform": pf,
            "backend_ok": ok,
            "remediation": None if ok else "Install libsecret-tools: `sudo apt install libsecret-tools` or `sudo dnf install libsecret`.",
        }
    return {
        "platform": pf,
        "backend_ok": False,
        "remediation": "Unsupported platform; the pipeline only ships Keychain support for macOS and libsecret for Linux. Use environment variables on Windows.",
    }


def _ensure_backend() -> str:
    info = doctor()
    if not info["backend_ok"]:
        sys.stderr.write(f"keychain: backend unavailable on {info['platform']}\n")
        if info["remediation"]:
            sys.stderr.write(f"keychain: {info['remediation']}\n")
        sys.exit(2)
    return info["platform"]


def get(label: str, account: Optional[str] = None) -> Optional[str]:
    """Lookup by label first, then by service name. Two coexisting conventions:
    - User's manually-added tokens (e.g. mmerterden_Vercel_Access_Token) use -l (label).
    - credential-store.sh-managed items use -s (service).
    The helper finds either."""
    pf = _ensure_backend()
    acct = _account_default(account)
    if pf == "macos":
        rc, out, _ = _run(["/usr/bin/security", "find-generic-password", "-a", acct, "-l", label, "-w"])
        if rc == 0:
            return out.decode("utf-8", errors="replace").rstrip("\n")
        rc, out, _ = _run(["/usr/bin/security", "find-generic-password", "-a", acct, "-s", label, "-w"])
        if rc == 0:
            return out.decode("utf-8", errors="replace").rstrip("\n")
        # account-agnostic last resort  -  some entries were written without -a
        rc, out, _ = _run(["/usr/bin/security", "find-generic-password", "-l", label, "-w"])
        if rc == 0:
            return out.decode("utf-8", errors="replace").rstrip("\n")
        rc, out, _ = _run(["/usr/bin/security", "find-generic-password", "-s", label, "-w"])
        if rc == 0:
            return out.decode("utf-8", errors="replace").rstrip("\n")
        return None
    # linux / secret-tool  -  store under both `label` and `service` schema attrs
    rc, out, _ = _run(["secret-tool", "lookup", "label", label, "account", acct])
    if rc == 0:
        return out.decode("utf-8", errors="replace").rstrip("\n")
    rc, out, _ = _run(["secret-tool", "lookup", "service", label])
    if rc == 0:
        return out.decode("utf-8", errors="replace").rstrip("\n")
    return None


def set_(label: str, value: str, account: Optional[str] = None) -> None:
    """Write the entry with both -l (label) and -s (service) set to the same id, so
    both lookup conventions succeed. -U overwrites if the item already exists."""
    pf = _ensure_backend()
    acct = _account_default(account)
    if pf == "macos":
        rc, _, err = _run(
            [
                "/usr/bin/security",
                "add-generic-password",
                "-a", acct,
                "-l", label,
                "-s", label,
                "-w", value,
                "-U",
            ]
        )
        if rc != 0:
            sys.stderr.write(err.decode("utf-8", errors="replace"))
            sys.exit(4)
        return
    # linux / secret-tool  -  store reads the secret from stdin; tag both schema attrs
    rc, _, err = _run(
        ["secret-tool", "store", "--label", label, "label", label, "service", label, "account", acct],
        input_value=value.encode("utf-8"),
    )
    if rc != 0:
        sys.stderr.write(err.decode("utf-8", errors="replace"))
        sys.exit(4)


def delete(label: str, account: Optional[str] = None) -> bool:
    """Delete by either convention. Returns True if any matching item was removed."""
    pf = _ensure_backend()
    acct = _account_default(account)
    if pf == "macos":
        deleted = False
        for flag in ("-l", "-s"):
            while True:
                rc, _, _ = _run(["/usr/bin/security", "delete-generic-password", "-a", acct, flag, label])
                if rc != 0:
                    break
                deleted = True
        for flag in ("-l", "-s"):
            while True:
                rc, _, _ = _run(["/usr/bin/security", "delete-generic-password", flag, label])
                if rc != 0:
                    break
                deleted = True
        return deleted
    deleted = False
    rc, _, _ = _run(["secret-tool", "clear", "label", label, "account", acct])
    if rc == 0:
        deleted = True
    rc, _, _ = _run(["secret-tool", "clear", "service", label])
    if rc == 0:
        deleted = True
    return deleted


def list_labels(filter_substring: Optional[str] = None) -> list[str]:
    pf = _ensure_backend()
    if pf != "macos":
        # secret-tool has no native list; we can't enumerate all entries portably.
        return []
    # `security dump-keychain` prints every item; we extract the 0x00000007 (label) attr.
    rc, out, _ = _run(["/usr/bin/security", "dump-keychain"])
    if rc != 0:
        return []
    labels: list[str] = []
    for raw in out.decode("utf-8", errors="replace").splitlines():
        line = raw.strip()
        # The label appears as: 0x00000007 <blob>="some label here"
        if "0x00000007" in line and '="' in line:
            value = line.split('="', 1)[1].rstrip('"')
            labels.append(value)
    if filter_substring:
        needle = filter_substring.lower()
        labels = [l for l in labels if needle in l.lower()]
    # Stable, dedupe, alphabetical.
    return sorted(set(labels))


def main(argv: list[str]) -> int:
    parser = argparse.ArgumentParser(prog="keychain.py", description=__doc__.split("\n", 1)[0])
    sub = parser.add_subparsers(dest="cmd", required=True)

    g = sub.add_parser("get")
    g.add_argument("label")
    g.add_argument("--account")

    s = sub.add_parser("set")
    s.add_argument("label")
    s.add_argument("value", help="Use '-' to read from stdin (preferred  -  keeps secrets out of shell history).")
    s.add_argument("--account")

    d = sub.add_parser("delete")
    d.add_argument("label")
    d.add_argument("--account")

    l = sub.add_parser("list")
    l.add_argument("--match")

    sub.add_parser("doctor")

    ns = parser.parse_args(argv)

    if ns.cmd == "get":
        v = get(ns.label, ns.account)
        if v is None:
            return 1
        sys.stdout.write(v)
        return 0
    if ns.cmd == "set":
        value = sys.stdin.read() if ns.value == "-" else ns.value
        set_(ns.label, value, ns.account)
        return 0
    if ns.cmd == "delete":
        return 0 if delete(ns.label, ns.account) else 1
    if ns.cmd == "list":
        for label in list_labels(ns.match):
            print(label)
        return 0
    if ns.cmd == "doctor":
        info = doctor()
        print(json.dumps(info, indent=2))
        return 0 if info["backend_ok"] else 2
    return 3


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
