#!/usr/bin/env python3
"""One read-only VICIdial user_details request. Python 3 standard library only.

prepare prompts for credentials and creates a new private JSON file.
read sends one HTTPS POST; no redirects, retries, writes, or TLS bypass.
validate checks a local response fixture without contacting a server.
"""
import argparse
import csv
import getpass
import http.client
import io
import json
import os
from pathlib import Path
import re
import ssl
import stat
import sys
import urllib.error
import urllib.parse
import urllib.request

HEADER = ["user", "full_name", "user_group", "user_level", "active"]
DEFAULT = Path.home() / ".config" / "vicigeek-api" / "credentials.json"


def endpoint(value):
    parsed = urllib.parse.urlsplit(value)
    if (parsed.scheme != "https" or not parsed.hostname or parsed.username
            or parsed.password or parsed.query or parsed.fragment
            or not parsed.path.endswith("/vicidial/non_agent_api.php")):
        raise ValueError("Use the approved HTTPS Non-Agent API URL without credentials, query, or fragment.")
    return value


def identifier(value):
    if not re.fullmatch(r"[A-Za-z0-9_]{2,20}", value):
        raise ValueError("Use a 2–20 character synthetic user ID containing letters, digits, or underscore.")
    return value


def parse_response(body, target):
    text = body.decode("utf-8-sig", errors="strict").strip()
    if any(line.startswith(("ERROR:", "NOTICE:")) for line in text.splitlines()):
        if "USER NOT FOUND" in text:
            raise ValueError("Target user not found: confirm the synthetic ID in Admin.")
        if "PERMISSION" in text:
            raise ValueError("Permission rejected: have the owner review this function's access requirements.")
        if "INVALID SEARCH" in text:
            raise ValueError("Search rejected: confirm the target ID and function parameters.")
        raise ValueError("API rejected the request: inspect authentication and the installed reference privately.")
    rows = list(csv.reader(io.StringIO(text), strict=True))
    if len(rows) != 2 or rows[0] != HEADER or len(rows[1]) != len(HEADER):
        raise ValueError("Unexpected response: require the five-column CSV header and exactly one record.")
    row = dict(zip(HEADER, rows[1]))
    if row["user"] != target:
        raise ValueError("Returned user differs from the requested target; do not accept this response.")
    if not row["user_level"].isdigit() or row["active"] not in ("Y", "N"):
        raise ValueError("Unexpected level or active value; check the installed response contract.")
    return row


def prepare(path):
    values = {
        "endpoint": endpoint(input("Approved HTTPS Non-Agent API URL: ").strip()),
        "user": identifier(input("Dedicated API username: ").strip()),
        "target": identifier(input("Existing synthetic target username: ").strip()),
        "pass": getpass.getpass("API password (hidden): "),
    }
    if not values["pass"]:
        raise ValueError("Password must not be empty.")
    path.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
    directory = path.parent.stat()
    if directory.st_uid != os.getuid() or stat.S_IMODE(directory.st_mode) & 0o077:
        raise ValueError("Credential directory must belong to you and have no group/other access (mode 0700).")
    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    with os.fdopen(fd, "w", encoding="utf-8") as stream:
        json.dump(values, stream)
        stream.write("\n")
    print(f"Created private credential file: {path}")
    print("No network request was made. Next: python3 user-details.py read")


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


def read(path):
    fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
    with os.fdopen(fd, encoding="utf-8") as stream:
        info = os.fstat(stream.fileno())
        if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o600:
            raise ValueError("Credential file must be your regular mode-0600 file.")
        values = json.load(stream)
    url = endpoint(values["endpoint"])
    target = identifier(values["target"])
    form = urllib.parse.urlencode({
        "source": "vicigeek-read", "user": identifier(values["user"]),
        "pass": values["pass"], "function": "user_details",
        "agent_user": target, "stage": "csv", "header": "YES",
    }).encode("utf-8")
    request = urllib.request.Request(url, data=form, method="POST", headers={
        "Content-Type": "application/x-www-form-urlencoded",
    })
    opener = urllib.request.build_opener(
        NoRedirect(), urllib.request.HTTPSHandler(context=ssl.create_default_context()))
    with opener.open(request, timeout=20) as response:
        if response.status != 200:
            raise ValueError("Unexpected HTTP status; no API result accepted.")
        body = response.read(65537)
    if len(body) > 65536:
        raise ValueError("Response exceeds this single-record example's 64 KiB limit.")
    show(parse_response(body, target))


def show(row):
    print("One matching user_details record (compare every field with Admin):")
    for field in HEADER:
        print(f"{field}: {row[field]!r}")
    print("This read does not verify write permissions, an agent session, or call handling.")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("action", choices=["prepare", "read", "validate"])
    parser.add_argument("--credentials", type=Path, default=DEFAULT)
    parser.add_argument("--response", type=Path)
    parser.add_argument("--target")
    args = parser.parse_args()
    try:
        if args.action == "prepare":
            prepare(args.credentials.expanduser())
        elif args.action == "read":
            read(args.credentials.expanduser())
        else:
            if not args.response or not args.target:
                parser.error("validate requires --response and --target")
            show(parse_response(args.response.read_bytes(), identifier(args.target)))
    except urllib.error.HTTPError as exc:
        print(f"HTTP {exc.code}: no API result accepted; redirects are not followed.", file=sys.stderr)
        return 1
    except urllib.error.URLError:
        print("Network/TLS failure: check DNS, reachability, and certificate trust; do not bypass TLS.", file=sys.stderr)
        return 1
    except TimeoutError:
        print("Network timeout: no complete response was accepted; check reachability with the owner.", file=sys.stderr)
        return 1
    except http.client.HTTPException:
        print("Incomplete or malformed HTTP response: no API result accepted.", file=sys.stderr)
        return 1
    except FileExistsError:
        print("Credential file already exists; inspect it privately or choose a new --credentials path.", file=sys.stderr)
        return 1
    except (ValueError, OSError, KeyError, csv.Error) as exc:
        # JSON parsing diagnostics can include source text; do not print secrets.
        if isinstance(exc, json.JSONDecodeError):
            print("Invalid credential JSON; inspect the file privately.", file=sys.stderr)
        elif isinstance(exc, (OSError, KeyError)):
            print("Required file, private permissions, or credential field unavailable; inspect setup privately.", file=sys.stderr)
        else:
            print(str(exc), file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
