"""Example 12 — Access: share a dataset, publish it, and hand a program a run token.

Everything the earlier examples built was **private**: readable by you and nobody else.
This is how you change that — and how a program acts for you without your login.

The unit of sharing is the resource, and the mechanism is a **grant** — one resource, one
team, level ``read`` or ``write``. Publishing is not a separate flag: it is a ``read``
grant to the reserved global team ``everyone``, so publishing and withdrawing are the same
verb in both directions and there is nothing special to undo.

The part worth seeing run: **read access on a dataset is enough for its samples.** A team
granted the dataset can read every sample in it without any access to the store it was
built from. That is the case you actually want — share the training set, not the raw
corpus — and it is safe because a dataset is frozen, so the grant cannot silently widen
later.

Last, a **run token**: people log in, programs run on a run token. Minted from your client,
it acts as you — it reads the dataset you just built — but it never shares, so the grant
it attempts is refused. Revoking it locks it out.

    DATAVO_SERVER=https://dev.datavo.io python examples/12_share_a_dataset.py
"""

from __future__ import annotations

from _common import (
    build_dataset,
    cleanup,
    cleanup_datasets,
    cleanup_run_token,
    make_client,
    resolve_server,
    seed_source_store,
    step,
    unique,
)

from datavo_sdk import (
    DatavoApiError,
    DatavoClient,
    KeyGroupSpec,
    SampleStream,
    ShardPlan,
    SourceSpec,
    mint_run_token,
    revoke_run_token,
    token_id_of,
)

KEYS = ["audio.wav", "transcript.txt"]
SAMPLES = [
    {"__key__": f"{i:04d}", "audio.wav": f"audio-{i}".encode(), "transcript.txt": f"utterance {i}"}
    for i in range(1, 6)
]

GLOBAL_TEAM = "everyone"


def describe_sharing(client, dataset: str) -> None:
    """Print the grants and the label a client derives from them."""
    grants = client.list_resource_grants("dataset", dataset)
    shared_with = ", ".join(f"{g.team_id}:{g.level}" for g in grants.grants) or "nobody"
    print(f"    owner={grants.owner_team_id}  sharing={grants.sharing}  shared_with={shared_with}")


def main() -> None:
    server = resolve_server()
    client = make_client(server)
    store = unique("share_store")
    dataset = unique("share_dataset")
    token = None

    try:
        step("Who am I, and where do new resources land?")
        me = client.get_auth_status()
        # A resource you create lands in your PERSONAL team unless you name one; there
        # is no setting to change that, you name the team at creation.
        print(f"    user={me.user_id}  teams={[team.id for team in me.teams]}")
        for team in me.teams:
            print(f"    team {team.id}{' (implicit)' if team.implicit else ''}")

        step("Build a store and a dataset over it")
        seed_source_store(client, store, samples=SAMPLES)
        build_dataset(
            client,
            dataset,
            sources=[SourceSpec(sample_store=store, keys=KEYS)],
            key_groups=[KeyGroupSpec(name="all", keys=KEYS)],
            shard_plan=ShardPlan(shard_size=100),
        )

        step("Both are private — no grants at all")
        describe_sharing(client, dataset)

        step("Pick a team to share with")
        # The teams you can see: your own, plus every team owning something you can read.
        # Any one that is not the owner and not the global team will do; this picks whatever
        # is available rather than assuming a name.
        owner = client.list_resource_grants("dataset", dataset).owner_team_id
        candidates = [
            team.id
            for team in client.list_teams().teams
            if team.id != owner and team.kind != "global"
        ]
        if not candidates:
            print("    no second team on this deployment — skipping the named-team grant")
        else:
            target = candidates[0]
            step(f"Share the dataset with {target} at read")
            client.grant_resource_access("dataset", dataset, team=target, level="read")
            describe_sharing(client, dataset)
            print(f"    {target} can now read this dataset's samples —")
            print(f"    and still has no access to {store}, the store it was built from.")

            step(f"Unshare it from {target}")
            # The same verb backwards: nothing about revoking is a special case.
            client.revoke_resource_access("dataset", dataset, team=target)
            describe_sharing(client, dataset)

        step("Find a person by their sign-in address")
        # The team list does not list people. To share with one person, name them: their
        # whole sign-in address resolves to their personal team, and a grant to that team is
        # a grant to them. Resolving your own address shows the shape.
        if me.preferred_username:
            person = client.resolve_team_for_person(me.preferred_username)
            print(f"    {me.preferred_username} -> {person.id} ({person.display_name})")

        step("Publish it to everyone")
        # No visibility flag: "public" IS a read grant to the global team.
        client.grant_resource_access("dataset", dataset, team=GLOBAL_TEAM, level="read")
        describe_sharing(client, dataset)

        step("A published dataset still reads exactly the same")
        rows = list(SampleStream.from_dataset(dataset, keys=KEYS, client=client))
        print(f"    read {len(rows)} samples — sharing changes who may read, not what is there")

        step("Withdraw it")
        client.revoke_resource_access("dataset", dataset, team=GLOBAL_TEAM)
        describe_sharing(client, dataset)

        step("Hand a program a run token")
        # People log in; programs run on a run token. It acts as you — your teams, the
        # contributor role — and its client takes it to the one host that accepts it.
        token, expires_at = mint_run_token(client, ttl_seconds=600)
        print(f"    minted {token_id_of(token)}, expires {expires_at.isoformat(timespec='minutes')}")
        if not client.get_public_config().processing_api_base_url:
            # A local stack has one host and no processing host to hold a run token to its rules.
            print("    this server has no processing host for run tokens — skipping the program's calls")
        else:
            with DatavoClient(server_url=server, auth_token=token) as program:
                rows = list(SampleStream.from_dataset(dataset, keys=KEYS, client=program))
                print(f"    the program read {len(rows)} samples of your private dataset, as you")
                try:
                    program.grant_resource_access("dataset", dataset, team=GLOBAL_TEAM, level="read")
                except DatavoApiError as exc:
                    print(f"    …but it may not share it ({exc.status_code}): sharing is a person's decision")
                else:
                    raise RuntimeError("a run token was allowed to share — it never should be")

                step("Revoke the token")
                revoke_run_token(token, server_url=server)
                try:
                    program.get_dataset(dataset)
                except DatavoApiError as exc:
                    print(f"    the program is locked out ({exc.status_code})")
                else:
                    raise RuntimeError("a revoked run token still worked")
    finally:
        cleanup_run_token(token, server=server)
        cleanup_datasets(client, dataset)
        cleanup(client, store)


if __name__ == "__main__":
    main()
