// MCPProfilesService + MCPCatalogService -- the IA-parity public CRUD
// surface for fraser's MCP feature set.
//
// Two services live in the same package because they share the MCP
// resource family:
//   - MCPProfilesService manages MCP server registrations scoped to
//     (user_id, customer_id, project_id) in mcp_profiles. A profile is
//     what an end user adds when they want their data plane to call out
//     to a remote MCP server they own, with API-key or OAuth credentials
//     attached.
//   - MCPCatalogService manages the platform-curated "official" MCP
//     server catalog (mcp_catalog_servers table). Operators publish a
//     server here; end users see it in the dashboard and CLI when they
//     pick what to add.
//
// Data access is delegated to the existing Drizzle-owned tables
// (`mcp_profiles`, `mcp_catalog_servers`); handlers in svc/internal/mcpsvc
// are a thin REST/CLI facade over those rows -- no parallel schema
// lives here.
//
// Profiles are scoped per (user_id, customer_id, project_id). A profile
// owned by user A in (customer X, project P) is invisible to user A in
// (customer X, project Q) and to any other user. The CLI surface uses
// --user-id together with --customer-id and --project-id to identify the
// owning scope, matching how aikeys.v1 surfaces per-customer/per-project
// scoping today.
//
// The data-plane lookup path (tars.lookup.v1.GetMCPProfileByURL) is
// the read surface ext_authz uses with aggressive caching; this service
// is the slower CRUD path for customer/operator use.
syntax = "proto3";

package tars.mcp.v1;

import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "tars/auth/v1/auth_options.proto";
import "tars/cli/v1/cli.proto";
import "tars/common/v1/types.proto";
import "tars/docs/v1/docs.proto";

option go_package = "github.com/tetrateio/fraser/api/tars/mcp/v1;mcpv1";

// MCPProfile is a user-registered MCP server: a remote URL the data
// plane proxies for the owner, optionally guarded by an API key or an
// OAuth resource policy.
message MCPProfile {
  // Server-assigned uuid row id. Output-only.
  string id = 1 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Owning user id (authn.user.id). Defaults to the caller's session
  // user when empty; immutable once set.
  string user_id = 2;

  // Owning customer scope. Defaults to the caller's session customer
  // when empty; immutable once set. A profile is only visible to
  // (user_id, customer_id, project_id) tuples that match.
  string customer_id = 10;

  // Owning project scope. Defaults to the caller's session project when
  // empty; immutable once set.
  string project_id = 11;

  // Human-friendly display name. Unique per user.
  string name = 3 [(google.api.field_behavior) = REQUIRED];

  // Optional freeform description.
  string description = 4;

  // URL path the data plane exposes for this profile, e.g.
  // "/mcp/<user-hash>-<profile-hash>". Server-assigned on create.
  string url = 5 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Lifecycle status: "active" | "pending" | "error".
  string status = 6 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Last error from the reconciler when status is "error".
  string error_message = 7 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Security policy type: "oauth" | "api_key" | "none".
  string security_policy_type = 8 [(google.api.field_behavior) = REQUIRED];

  // JSON-encoded security policy value. Shape depends on
  // security_policy_type -- see schema docs on the mcp_profiles table.
  // Sensitive material (raw api keys, oauth issuer secrets) is stored
  // server-side; responses redact secret bytes.
  string security_policy_value = 9;

  // Row creation time. Output-only.
  google.protobuf.Timestamp created_at = 50 [(google.api.field_behavior) = OUTPUT_ONLY];
  // Last mutation time. Output-only.
  google.protobuf.Timestamp updated_at = 51 [(google.api.field_behavior) = OUTPUT_ONLY];
}

// MCPCatalogServer is a platform-curated MCP server registration.
// Mirrors the mcp_catalog_servers row 1:1.
message MCPCatalogServer {
  // Stable string id (e.g. "github", "jira"). Required on upsert.
  string id = 1 [(google.api.field_behavior) = REQUIRED];

  // Display name shown in the dashboard catalog.
  string name = 2 [(google.api.field_behavior) = REQUIRED];

  // Description text shown in the dashboard catalog.
  string description = 3;

  // Customer-facing MCP server URL.
  string url = 4 [(google.api.field_behavior) = REQUIRED];

  // Field 5 (tars_url) was removed: it was written and surfaced but read
  // by no dataplane consumer -- dead storage. Reserved so the number/name
  // are never reused.
  reserved 5;
  reserved "tars_url";

  // Optional icon URL displayed in the dashboard catalog.
  string icon_url = 6;

  // Category tags ("git", "issue-tracker", ...). Empty list permitted.
  repeated string categories = 7;

  // Optional free-form note shown alongside the catalog entry.
  string note = 8;

  // Authentication kind label ("oauth", "api_key", "none"). Free-form
  // string -- the catalog renderer interprets it.
  string authentication = 9;

  // True when this catalog entry requires a credential from the user
  // (used by the dashboard "Add" flow to pre-populate credential UI).
  bool requires_auth = 10;

  // True when the user must register a profile before using this
  // catalog entry (rather than just adding a backend selector).
  bool requires_profile = 11;

  // Operator-controlled visibility toggle. Disabled servers stay in
  // the table but are hidden from the customer-facing list.
  bool is_enabled = 12;

  // Row creation time. Output-only.
  google.protobuf.Timestamp created_at = 50 [(google.api.field_behavior) = OUTPUT_ONLY];
  // Last mutation time. Output-only.
  google.protobuf.Timestamp updated_at = 51 [(google.api.field_behavior) = OUTPUT_ONLY];
}

// MCPProfilesService is the (user, customer, project)-scoped CRUD
// surface for mcp_profiles.
service MCPProfilesService {
  // CreateMCPProfile registers a new MCP server for a user.
  rpc CreateMCPProfile(CreateMCPProfileRequest) returns (MCPProfile) {
    option (tars.docs.v1.title) = "Create an MCP profile";
    option (tars.docs.v1.group) = "MCP Profiles";
    option (google.api.http) = {
      post: "/v1/customers/{customer_id}/projects/{project_id}/users/{user_id}/mcp-profiles"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: [
        "mcp",
        "profiles",
        "create"
      ]
    };
  }

  // GetMCPProfile returns a single profile by uuid.
  rpc GetMCPProfile(GetMCPProfileRequest) returns (MCPProfile) {
    option (tars.docs.v1.title) = "Get an MCP profile";
    option (tars.docs.v1.group) = "MCP Profiles";
    option (google.api.http) = {get: "/v1/customers/{customer_id}/projects/{project_id}/users/{user_id}/mcp-profiles/{profile_id}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: [
        "mcp",
        "profiles",
        "get"
      ]
    };
  }

  // ListMCPProfiles lists every profile owned by the given
  // (user_id, customer_id, project_id) scope.
  rpc ListMCPProfiles(ListMCPProfilesRequest) returns (ListMCPProfilesResponse) {
    option (tars.docs.v1.title) = "List MCP profiles";
    option (tars.docs.v1.group) = "MCP Profiles";
    option (google.api.http) = {get: "/v1/customers/{customer_id}/projects/{project_id}/users/{user_id}/mcp-profiles"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: [
        "mcp",
        "profiles",
        "list"
      ]
    };
  }

  // UpdateMCPProfile updates mutable profile fields. Omitted scalar
  // strings ("") leave the existing value untouched.
  rpc UpdateMCPProfile(UpdateMCPProfileRequest) returns (MCPProfile) {
    option (tars.docs.v1.title) = "Update an MCP profile";
    option (tars.docs.v1.group) = "MCP Profiles";
    option (google.api.http) = {
      patch: "/v1/customers/{customer_id}/projects/{project_id}/users/{user_id}/mcp-profiles/{profile_id}"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: [
        "mcp",
        "profiles",
        "update"
      ]
    };
  }

  // DeleteMCPProfile soft-deletes a profile (deleted_at set; row kept
  // for FK integrity against historical request logs).
  rpc DeleteMCPProfile(DeleteMCPProfileRequest) returns (google.protobuf.Empty) {
    option (tars.docs.v1.title) = "Delete an MCP profile";
    option (tars.docs.v1.group) = "MCP Profiles";
    option (google.api.http) = {delete: "/v1/customers/{customer_id}/projects/{project_id}/users/{user_id}/mcp-profiles/{profile_id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: [
        "mcp",
        "profiles",
        "delete"
      ]
    };
  }
}

// MCPCatalogService manages the platform-curated catalog of MCP
// servers. Operator-only surface -- end users only read from it via
// the dashboard.
service MCPCatalogService {
  // UpsertCatalogServer creates or replaces a catalog entry. Idempotent
  // on `id`. Operator-only.
  rpc UpsertCatalogServer(UpsertCatalogServerRequest) returns (MCPCatalogServer) {
    option (tars.docs.v1.title) = "Create or update an MCP catalog server";
    option (tars.docs.v1.group) = "MCP Catalog";
    option (google.api.http) = {
      post: "/v1/mcp/catalog/servers"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "mcp",
        "catalog",
        "upsert"
      ]
    };
  }

  // GetCatalogServer returns a single catalog entry by id.
  rpc GetCatalogServer(GetCatalogServerRequest) returns (MCPCatalogServer) {
    option (tars.docs.v1.title) = "Get an MCP catalog server";
    option (tars.docs.v1.group) = "MCP Catalog";
    option (google.api.http) = {get: "/v1/mcp/catalog/servers/{id}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "mcp",
        "catalog",
        "get"
      ]
      primary_flag: "id"
    };
  }

  // ListCatalogServers returns every catalog entry, including disabled
  // rows so operators can re-enable them. The dashboard's read-only
  // view filters to is_enabled=true.
  rpc ListCatalogServers(ListCatalogServersRequest) returns (ListCatalogServersResponse) {
    option (tars.docs.v1.title) = "List MCP catalog servers";
    option (tars.docs.v1.group) = "MCP Catalog";
    option (google.api.http) = {get: "/v1/mcp/catalog/servers"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "mcp",
        "catalog",
        "list"
      ]
    };
  }

  // SetCatalogServerEnabled flips the operator visibility toggle on a
  // catalog entry. Split from UpsertCatalogServer so a re-upsert that
  // refreshes content fields (display_name, url, ...) cannot silently
  // re-enable a previously-disabled row through the proto bool zero
  // value.
  rpc SetCatalogServerEnabled(SetCatalogServerEnabledRequest) returns (MCPCatalogServer) {
    option (tars.docs.v1.title) = "Toggle MCP catalog server visibility";
    option (tars.docs.v1.group) = "MCP Catalog";
    option (google.api.http) = {
      post: "/v1/mcp/catalog/servers/{id}/enabled"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "mcp",
        "catalog",
        "set-enabled"
      ]
    };
  }

  // DeleteCatalogServer removes a catalog entry by id. Operator-only.
  rpc DeleteCatalogServer(DeleteCatalogServerRequest) returns (google.protobuf.Empty) {
    option (tars.docs.v1.title) = "Delete an MCP catalog server";
    option (tars.docs.v1.group) = "MCP Catalog";
    option (google.api.http) = {delete: "/v1/mcp/catalog/servers/{id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
      scopes: ["admin"]
    };
    option (tars.cli.v1.command) = {
      path: [
        "mcp",
        "catalog",
        "delete"
      ]
      primary_flag: "id"
    };
  }
}

// ── MCPProfilesService request/response messages ──────────────────────
//
// Request messages flatten the MCPProfile resource fields into scalar
// top-level fields so the CLI generator can surface each one as a
// flag. The HTTP body shape stays semantically equivalent.

message CreateMCPProfileRequest {
  // Defaults to the caller's session user when empty.
  string user_id = 1;

  // Required: human-friendly profile name.
  string name = 2 [(google.api.field_behavior) = REQUIRED];

  // Optional description.
  string description = 3;

  // Required: security policy type ("oauth" | "api_key" | "none").
  string security_policy_type = 4 [(google.api.field_behavior) = REQUIRED];

  // Optional: JSON-encoded security policy value. Empty for "none".
  string security_policy_value = 5;

  // Defaults to the caller's session customer when empty.
  string customer_id = 6;

  // Defaults to the caller's session project when empty.
  string project_id = 7;
}

message GetMCPProfileRequest {
  // Defaults to the caller's session user when empty.
  string user_id = 1;
  // Required: uuid of the profile to fetch (MCPProfile.id).
  string profile_id = 2 [(google.api.field_behavior) = REQUIRED];
  // Defaults to the caller's session customer when empty.
  string customer_id = 3;
  // Defaults to the caller's session project when empty.
  string project_id = 4;
}

message ListMCPProfilesRequest {
  // Defaults to the caller's session user when empty.
  string user_id = 1;
  // Cursor-pagination inputs (page size + continuation token).
  tars.common.v1.PageRequest page = 2;
  // Defaults to the caller's session customer when empty.
  string customer_id = 3;
  // Defaults to the caller's session project when empty.
  string project_id = 4;
}

message ListMCPProfilesResponse {
  // Profiles owned by the requested scope, one page's worth.
  repeated MCPProfile profiles = 1;
  // Cursor-pagination outputs (next-page token).
  tars.common.v1.PageResponse page = 2;
}

message UpdateMCPProfileRequest {
  // Defaults to the caller's session user when empty.
  string user_id = 1;
  // Required: uuid of the profile to update (MCPProfile.id).
  string profile_id = 2 [(google.api.field_behavior) = REQUIRED];

  // Mutable scalar fields. Empty string leaves the existing value
  // untouched -- callers explicitly pass " " when they want to clear
  // a free-form description (the server trims that to "").
  string name = 3;
  string description = 4;
  string security_policy_type = 5;
  string security_policy_value = 6;

  // Defaults to the caller's session customer when empty.
  string customer_id = 7;
  // Defaults to the caller's session project when empty.
  string project_id = 8;
}

message DeleteMCPProfileRequest {
  // Defaults to the caller's session user when empty.
  string user_id = 1;
  // Required: uuid of the profile to soft-delete (MCPProfile.id).
  string profile_id = 2 [(google.api.field_behavior) = REQUIRED];
  // Defaults to the caller's session customer when empty.
  string customer_id = 3;
  // Defaults to the caller's session project when empty.
  string project_id = 4;
}

// ── MCPCatalogService request/response messages ───────────────────────

message UpsertCatalogServerRequest {
  // Required: stable catalog entry id (e.g. "github").
  string id = 1 [(google.api.field_behavior) = REQUIRED];

  // Required: display name.
  string name = 2 [(google.api.field_behavior) = REQUIRED];

  // Optional description.
  string description = 3;

  // Required: customer-facing URL. The flag is renamed to
  // --catalog-url to avoid colliding with the root command's
  // persistent --base-url override.
  string url = 4 [
    (google.api.field_behavior) = REQUIRED,
    (tars.cli.v1.field) = {flag: "catalog-url"}
  ];

  // Field 5 (tars_url) was removed: it was dead storage (written and
  // surfaced but read by no dataplane consumer). Reserved so the
  // number/name are never reused.
  reserved 5;
  reserved "tars_url";

  // Optional icon URL.
  string icon_url = 6;

  // Optional category tags.
  repeated string categories = 7;

  // Optional free-form note.
  string note = 8;

  // Optional authentication kind label.
  string authentication = 9;

  // Whether this catalog entry requires a credential.
  bool requires_auth = 10;

  // Whether the user must register a profile before using this entry.
  bool requires_profile = 11;

  // The operator visibility toggle is intentionally NOT exposed here.
  // Upsert mutates content-only fields; flipping `is_enabled` requires
  // the dedicated SetCatalogServerEnabled RPC. Re-upserting a disabled
  // row to refresh display_name/url keeps the row disabled.
  reserved 12;
  reserved "disabled";
}

message GetCatalogServerRequest {
  // Required: stable catalog entry id (e.g. "github").
  string id = 1 [(google.api.field_behavior) = REQUIRED];
}

message ListCatalogServersRequest {
  // When true, include is_enabled=false entries. The dashboard sets
  // this false; the operator CLI defaults to true so disabled servers
  // are visible.
  bool include_disabled = 1;

  // Cursor-pagination inputs (page size + continuation token).
  tars.common.v1.PageRequest page = 2;
}

message ListCatalogServersResponse {
  // Catalog entries for this page (disabled rows included per request).
  repeated MCPCatalogServer servers = 1;
  // Cursor-pagination outputs (next-page token).
  tars.common.v1.PageResponse page = 2;
}

message DeleteCatalogServerRequest {
  // Required: stable id of the catalog entry to delete (e.g. "github").
  string id = 1 [(google.api.field_behavior) = REQUIRED];
}

message SetCatalogServerEnabledRequest {
  // Stable catalog entry id, e.g. "github".
  string id = 1 [(google.api.field_behavior) = REQUIRED];

  // Target visibility. true -> entry is visible to the customer-facing
  // list; false -> hidden (row kept, is_enabled set to false).
  bool enabled = 2 [(google.api.field_behavior) = REQUIRED];
}
