52:def resolve_client_config(config=None) -> dict:
53-    """Return {client_id, client_secret, redirect_uri} for OAuth flows.
54-
55-    Priority: explicit config dict (per-user imported config) > DB row > env file path > error.
56-    """
57-    if config and config.client_id:
58-        return {
59-            "client_id": config.client_id,
60-            "client_secret": config.client_secret,  # decrypted property
61-            "redirect_uri": config.redirect_uri or f"{settings.BASE_URL}/auth/google/callback",
62-        }
63-    path = settings.GOOGLE_OAUTH_CLIENT_JSON_PATH
64-    if path and __import__("os").path.exists(path):
65-        with open(path) as f:
66-            data = json.load(f)
67-        inst = data.get("installed") or data.get("web")
68-        if not inst:
69-            raise GmailOauthError("Invalid Google OAuth client JSON: no 'installed' or 'web' key")
70-        return {
71-            "client_id": inst["client_id"],
72-            "client_secret": inst["client_secret"],
73-            "redirect_uri": f"{settings.BASE_URL}/auth/google/callback",
74-        }
75-    raise GmailOauthError(
76-        "No Google OAuth client configuration available. Import a client_secret.json in "
77-        "Gmail Accounts → Import Gmail Configuration, or set GOOGLE_OAUTH_CLIENT_JSON_PATH."
--
81:def parse_client_json(content: bytes) -> dict:
82-    """Validate an uploaded client_secret JSON. Returns normalized dict; raises on invalid."""
83-    try:
84-        data = json.loads(content.decode("utf-8"))
85-    except Exception as e:
86-        raise GmailOauthError(f"File is not valid JSON: {e}")
87-    inst = data.get("installed") or data.get("web")
88-    if not inst or not inst.get("client_id") or not inst.get("client_secret"):
89-        raise GmailOauthError("JSON must contain an 'installed' or 'web' object with client_id and client_secret")
90-    return {
91-        "project_id": inst.get("project", "") or inst.get("project_id", ""),
92-        "client_id": inst["client_id"],
93-        "client_secret": inst["client_secret"],
94-        "redirect_uris": inst.get("redirect_uris", []),
95-        "auth_uri": inst.get("auth_uri", ""),
96-        "token_uri": inst.get("token_uri", ""),
97-    }
98-
99-
100-# ---------------------------------------------------------------------------
101-# Authorization flow
102-# ---------------------------------------------------------------------------
103-def generate_state() -> str:
104-    return secrets.token_urlsafe(32)
105-
106-
--
152:def revoke_token(token: str) -> bool:
153-    """Revoke a token at Google. Returns True if revoked successfully."""
154-    if not token:
155-        return False
156-    try:
157-        resp = requests.post(GOOGLE_REVOKE_URL, params={"token": token}, timeout=15)
158-        return resp.status_code == 200
159-    except Exception:
160-        return False
161-
162-
163-def get_userinfo(access_token: str) -> dict:
164-    resp = requests.get(GOOGLE_USERINFO_URL, headers={"Authorization": f"Bearer {access_token}"}, timeout=15)
165-    if resp.status_code != 200:
166-        raise GmailOauthError(f"userinfo failed: {resp.status_code}")
167-    return resp.json()
===== OAuthClientConfig
class OAuthClientConfig(Base, TimestampMixin):
    """An imported Google OAuth client configuration (from client_secret JSON).
    Secrets stored encrypted. Multiple configs may exist (e.g. per project)."""
    __tablename__ = "oauth_client_configs"
    id = Column(Integer, primary_key=True)
    name = Column(String(200), nullable=False)
    client_id = Column(String(300), nullable=False)
    client_secret_enc = Column(String(600), nullable=False)  # encrypted
    redirect_uri = Column(String(500), default="")
    project_id = Column(String(200), default="")
    is_active = Column(Boolean, default=True, nullable=False)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"))

    @property
    def client_secret(self) -> str:
        return encryption.decrypt(self.client_secret_enc)

    @client_secret.setter
    def client_secret(self, value: str) -> None:
        self.client_secret_enc = encryption.encrypt(value)


class GmailAccount(Base, TimestampMixin):
