#!/usr/bin/env python3
"""Reset an existing WhaleTodo account through the owner's Supabase Auth admin API.

Run on a trusted computer. Credentials are prompted without echo, never saved,
and never sent to WhaleTodo. No third-party Python packages are required.
"""

import argparse
import getpass
import json
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
import uuid


class RecoveryError(Exception):
    pass


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        raise RecoveryError("Project redirected the request. Verify its URL; no credentials were forwarded.")


def project_url(value: str, allow_local: bool = False, allow_custom_domain: bool = False) -> str:
    url = urllib.parse.urlsplit(value.strip())
    local = allow_local and url.hostname in ("localhost", "127.0.0.1", "::1")
    if (url.scheme != "https" and not (local and url.scheme == "http")) or not url.hostname:
        raise RecoveryError("Use your HTTPS Project URL, copied from Supabase Connect.")
    if url.username or url.password or url.query or url.fragment or url.path not in ("", "/"):
        raise RecoveryError("Use the project origin only, without credentials, a path or query.")
    if not local and not allow_custom_domain and not re.fullmatch(r"[a-z0-9-]+\.supabase\.co", url.hostname):
        raise RecoveryError("Use the project's supabase.co URL. A verified self-hosted/custom domain requires --allow-custom-domain.")
    return urllib.parse.urlunsplit((url.scheme, url.netloc, "", "", ""))


def account_email(value: str) -> str:
    value = value.strip().lower()
    if re.fullmatch(r"[a-z0-9_-]{3,32}", value):
        return value + "@whaletodo.local"
    if re.fullmatch(r"[^\s@]+@[^\s@]+\.[^\s@]+", value):
        return value
    raise RecoveryError("Enter the existing WhaleTodo username, or a legacy account email.")


class AdminClient:
    def __init__(self, project: str, key: str):
        self.project = project
        self.key = key
        # Do not route privileged recovery requests through environment HTTP proxies.
        self.opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())

    def request(self, method: str, path: str, body=None):
        request = urllib.request.Request(
            self.project + "/auth/v1/admin/" + path,
            data=None if body is None else json.dumps(body).encode(),
            method=method,
            headers={"apikey": self.key, "Authorization": "Bearer " + self.key, "Content-Type": "application/json"},
        )
        try:
            with self.opener.open(request, timeout=30) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            # The response could contain personal data or a reflected credential.
            raise RecoveryError(f"Supabase returned HTTP {error.code}. Check the project and administrator key.") from None
        except (urllib.error.URLError, TimeoutError, ValueError):
            raise RecoveryError("Could not read the Supabase response. Check the project connection; no change was confirmed.") from None

    def find_user(self, email: str) -> str:
        for page in range(1, 10001):
            data = self.request("GET", f"users?page={page}&per_page=100")
            users = data.get("users", [])
            matched = [u for u in users if u.get("email", "").lower() == email]
            if len(matched) == 1:
                return str(uuid.UUID(matched[0]["id"]))
            if len(matched) > 1:
                raise RecoveryError("More than one account matched. Stop and inspect the project in Supabase.")
            if len(users) < 100:
                raise RecoveryError("That account was not found. Check the project and username; do not create a replacement account.")
        raise RecoveryError("Account lookup exceeded the limit. Inspect the project in Supabase.")

    def reset(self, uid: str, password: str):
        result = self.request("PUT", "users/" + str(uuid.UUID(uid)), {"password": password})
        user = result.get("user", result)
        if user.get("id") != uid:
            raise RecoveryError("The response did not confirm the original account. Check Supabase before retrying.")


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--project", help="Your Supabase Project URL (never an administrator key)")
    parser.add_argument("--username", help="Existing WhaleTodo username")
    parser.add_argument("--allow-custom-domain", action="store_true", help="Use a verified self-hosted or custom HTTPS project domain")
    args = parser.parse_args()
    try:
        project = project_url(args.project or input("Supabase Project URL: "), allow_custom_domain=args.allow_custom_domain)
        username = args.username or input("Existing WhaleTodo username: ")
        email = account_email(username)
        print("This will change only the password of the existing account in:", project)
        print("The account ID and its tasks will be preserved. Do not paste a publishable key here.")
        if input("Type the complete Project URL again to confirm the destination: ").strip().rstrip("/") != project:
            raise RecoveryError("Project confirmation did not match. No credentials were requested or sent.")
        key = getpass.getpass("Project Secret key (or legacy service_role key; input hidden): ").strip()
        if not key or key.startswith("sb_publishable_"):
            raise RecoveryError("Recovery requires the project's administrator key on this trusted computer.")
        client = AdminClient(project, key)
        uid = client.find_user(email)
        print("Found the existing account. No changes have been made.")
        password = getpass.getpass("New password (at least 16 characters; input hidden): ")
        if len(password) < 16:
            raise RecoveryError("The new password must have at least 16 characters.")
        if password != getpass.getpass("Repeat new password: "):
            raise RecoveryError("Passwords did not match. No changes were made.")
        if input("Type the existing username again to confirm password reset: ").strip().lower() != username.strip().lower():
            raise RecoveryError("Confirmation did not match. No changes were made.")
        client.reset(uid, password)
        print("Password reset confirmed. Sign in to WhaleTodo with the same project and username, and the new password.")
        return 0
    except RecoveryError as error:
        print(str(error), file=sys.stderr)
        return 1
    except (KeyboardInterrupt, EOFError):
        print("Cancelled. If cancellation happened during reset, verify sign-in before retrying.", file=sys.stderr)
        return 1


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