/**
 * Gitea API client for Vibn project provisioning.
 *
 * Used server-side only. Credentials come from env vars:
 *   GITEA_API_URL  — e.g. https://git.vibnai.com
 *   GITEA_API_TOKEN — admin token
 *   GITEA_ADMIN_USER — default owner for repos (e.g. "mark")
 */

const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com';
const GITEA_API_TOKEN = process.env.GITEA_API_TOKEN ?? '';
const GITEA_ADMIN_USER = process.env.GITEA_ADMIN_USER ?? 'mark';

export interface GiteaRepo {
  id: number;
  name: string;
  full_name: string;
  html_url: string;
  clone_url: string;
  ssh_url: string;
  private: boolean;
  default_branch: string;
}

export interface GiteaWebhook {
  id: number;
  type: string;
  active: boolean;
  config: {
    url: string;
    content_type: string;
    secret?: string;
  };
}

async function giteaFetch(path: string, options: RequestInit = {}) {
  const url = `${GITEA_API_URL}/api/v1${path}`;
  const res = await fetch(url, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `token ${GITEA_API_TOKEN}`,
      ...(options.headers ?? {}),
    },
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Gitea API error ${res.status} on ${path}: ${text}`);
  }

  if (res.status === 204) return null;
  return res.json();
}

/**
 * Create a new repo. By default creates under the admin user.
 * Pass `owner` to create under a specific user OR org — when the owner
 * is an org (or any user other than the token holder), Gitea requires
 * the org-scoped endpoint `/orgs/{owner}/repos`.
 */
export async function createRepo(
  name: string,
  opts: { description?: string; private?: boolean; owner?: string; auto_init?: boolean } = {}
): Promise<GiteaRepo> {
  const { description = '', private: isPrivate = true, owner = GITEA_ADMIN_USER, auto_init = true } = opts;

  const body = JSON.stringify({
    name,
    description,
    private: isPrivate,
    auto_init,
    default_branch: 'main',
  });

  // Token-owner repos use /user/repos; everything else (orgs, other users)
  // must go through /orgs/{owner}/repos.
  const path = owner === GITEA_ADMIN_USER ? `/user/repos` : `/orgs/${owner}/repos`;
  return giteaFetch(path, { method: 'POST', body });
}

// ──────────────────────────────────────────────────
// Organizations (per-workspace tenancy)
// ──────────────────────────────────────────────────

export interface GiteaOrg {
  id: number;
  username: string;        // org name (Gitea uses "username" for orgs too)
  full_name: string;
  description?: string;
  visibility: 'public' | 'private' | 'limited';
}

/**
 * Create a Gitea organization. Requires the admin token to have
 * permission to create orgs.
 */
export async function createOrg(opts: {
  name: string;
  fullName?: string;
  description?: string;
  visibility?: 'public' | 'private' | 'limited';
}): Promise<GiteaOrg> {
  const { name, fullName = name, description = '', visibility = 'private' } = opts;
  return giteaFetch(`/orgs`, {
    method: 'POST',
    body: JSON.stringify({
      username: name,
      full_name: fullName,
      description,
      visibility,
      repo_admin_change_team_access: true,
    }),
  });
}

export async function getOrg(name: string): Promise<GiteaOrg | null> {
  try {
    return await giteaFetch(`/orgs/${name}`);
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    if (msg.includes('404')) return null;
    throw err;
  }
}

/**
 * Add a Gitea user to an org's "Owners" team (full access to the org).
 * Falls back to the org's default team when "Owners" cannot be located.
 */
export async function addOrgOwner(orgName: string, username: string): Promise<void> {
  const teams = (await giteaFetch(`/orgs/${orgName}/teams`)) as Array<{ id: number; name: string }>;
  const owners = teams.find(t => t.name.toLowerCase() === 'owners') ?? teams[0];
  if (!owners) throw new Error(`No teams found for org ${orgName}`);
  await giteaFetch(`/teams/${owners.id}/members/${username}`, { method: 'PUT' });
}

export async function getUser(username: string): Promise<{ id: number; login: string } | null> {
  try {
    return await giteaFetch(`/users/${username}`);
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    if (msg.includes('404')) return null;
    throw err;
  }
}

// ──────────────────────────────────────────────────
// Admin: create users + mint PATs + team management
// (used for per-workspace bot provisioning)
// ──────────────────────────────────────────────────

export interface GiteaUser {
  id: number;
  login: string;
  full_name?: string;
  email?: string;
  active?: boolean;
}

/**
 * Create a Gitea user via the admin API. Requires the root admin token
 * to have the site-admin bit. `must_change_password: false` because the
 * bot never logs in interactively — only the PAT is used.
 */
export async function createUser(opts: {
  username: string;
  email: string;
  password: string;
  fullName?: string;
}): Promise<GiteaUser> {
  const created = await giteaFetch(`/admin/users`, {
    method: 'POST',
    body: JSON.stringify({
      username: opts.username,
      email: opts.email,
      password: opts.password,
      full_name: opts.fullName ?? opts.username,
      must_change_password: false,
      send_notify: false,
      source_id: 0,
    }),
  });

  // Gitea's admin-create endpoint returns users with `active=false` by
  // default (a quirk of the admin API — UI-created users skip email
  // verification but API-created ones don't). Inactive users fail
  // permission checks and cannot clone private repos, so we flip the
  // flag immediately via a PATCH. Idempotent: a second call is a noop.
  try {
    await giteaFetch(`/admin/users/${opts.username}`, {
      method: 'PATCH',
      body: JSON.stringify({
        source_id: 0,
        login_name: opts.username,
        active: true,
      }),
    });
    (created as GiteaUser).active = true;
  } catch (err) {
    console.warn('[gitea] failed to activate bot user', opts.username, err);
  }
  return created;
}

/**
 * Mint a Personal Access Token for a user.
 *
 * Gitea's `POST /users/{username}/tokens` endpoint is explicitly Basic-auth
 * only: neither an admin token nor the `Sudo` header work. So we require
 * the target user's password (we know it — we just created the user with
 * a random one and can discard it right after).
 *
 * Returns the plaintext token (`sha1` field) — store it encrypted;
 * Gitea never shows it again.
 */
export async function createAccessTokenFor(opts: {
  username: string;
  password: string;
  name: string;
  scopes?: string[];
}): Promise<{ id: number; name: string; sha1: string; token_last_eight: string }> {
  const {
    username,
    password,
    name,
    scopes = [
      'write:repository',
      'write:issue',
      'write:user',
      'write:organization',
    ],
  } = opts;
  const basic = Buffer.from(`${username}:${password}`).toString('base64');
  const url = `${GITEA_API_URL}/api/v1/users/${username}/tokens`;
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Basic ${basic}`,
    },
    body: JSON.stringify({ name, scopes }),
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Gitea token-mint error ${res.status} on /users/${username}/tokens: ${text}`);
  }
  return res.json();
}

/**
 * Admin: edit an existing user. Used during re-provisioning to reset
 * the bot's password so we can Basic-auth as it and mint a fresh PAT.
 */
export async function adminEditUser(opts: {
  username: string;
  password?: string;
  fullName?: string;
  email?: string;
}): Promise<GiteaUser> {
  const body: Record<string, unknown> = {
    source_id: 0,
    // Editing requires specifying login name explicitly.
    login_name: opts.username,
  };
  if (opts.password) body.password = opts.password;
  if (opts.fullName) body.full_name = opts.fullName;
  if (opts.email) body.email = opts.email;
  return giteaFetch(`/admin/users/${opts.username}`, {
    method: 'PATCH',
    body: JSON.stringify(body),
  });
}

export interface GiteaTeam {
  id: number;
  name: string;
  permission?: string;
}

export async function listOrgTeams(orgName: string): Promise<GiteaTeam[]> {
  return giteaFetch(`/orgs/${orgName}/teams`);
}

/**
 * Create a new team inside an org with a scoped permission.
 * `permission`: "read" | "write" | "admin" | "owner".
 * Used to give bot users less than full owner access.
 */
export async function createOrgTeam(opts: {
  org: string;
  name: string;
  description?: string;
  permission?: 'read' | 'write' | 'admin';
  includesAllRepos?: boolean;
}): Promise<GiteaTeam> {
  const {
    org,
    name,
    description = '',
    permission = 'write',
    includesAllRepos = true,
  } = opts;
  return giteaFetch(`/orgs/${org}/teams`, {
    method: 'POST',
    body: JSON.stringify({
      name,
      description,
      permission,
      includes_all_repositories: includesAllRepos,
      can_create_org_repo: true,
      units: [
        'repo.code',
        'repo.issues',
        'repo.pulls',
        'repo.releases',
        'repo.wiki',
        'repo.ext_wiki',
        'repo.ext_issues',
        'repo.projects',
        'repo.actions',
        'repo.packages',
      ],
    }),
  });
}

/** Add a user to a team by id. */
export async function addOrgTeamMember(teamId: number, username: string): Promise<void> {
  await giteaFetch(`/teams/${teamId}/members/${username}`, { method: 'PUT' });
}

/**
 * Ensure a team exists on an org with the requested permission, and
 * that `username` is a member of it. Idempotent.
 */
export async function ensureOrgTeamMembership(opts: {
  org: string;
  teamName: string;
  permission?: 'read' | 'write' | 'admin';
  username: string;
}): Promise<GiteaTeam> {
  const { org, teamName, permission = 'write', username } = opts;
  const teams = await listOrgTeams(org);
  let team = teams.find(t => t.name.toLowerCase() === teamName.toLowerCase());
  if (!team) {
    team = await createOrgTeam({
      org,
      name: teamName,
      description: `Vibn ${teamName} team`,
      permission,
    });
  }
  await addOrgTeamMember(team.id, username);
  return team;
}

// ──────────────────────────────────────────────────
// Admin: SSH keys on a user (for Coolify deploy-key flow)
// ──────────────────────────────────────────────────

export interface GiteaSshKey {
  id: number;
  key: string;
  title: string;
  fingerprint?: string;
  read_only?: boolean;
}

/**
 * Register an SSH public key under a target user via the admin API.
 * The resulting key gives anyone holding the matching private key the
 * same repo-read access as the user (bounded by that user's team
 * memberships — for bots, usually read/write on one org only).
 */
export async function adminAddUserSshKey(opts: {
  username: string;
  title: string;
  key: string;   // OpenSSH-format public key, e.g. "ssh-ed25519 AAAAC3... comment"
  readOnly?: boolean;
}): Promise<GiteaSshKey> {
  return giteaFetch(`/admin/users/${opts.username}/keys`, {
    method: 'POST',
    body: JSON.stringify({
      title: opts.title,
      key: opts.key,
      read_only: opts.readOnly ?? false,
    }),
  });
}

/**
 * List SSH keys for a user (admin view).
 */
export async function adminListUserSshKeys(username: string): Promise<GiteaSshKey[]> {
  return giteaFetch(`/users/${username}/keys`);
}

/**
 * Delete an SSH key by id (owned by a user). Used when rotating keys.
 */
export async function adminDeleteUserSshKey(keyId: number): Promise<void> {
  await giteaFetch(`/admin/users/keys/${keyId}`, { method: 'DELETE' });
}

/**
 * Get an existing repo.
 */
export async function getRepo(owner: string, repo: string): Promise<GiteaRepo | null> {
  try {
    return await giteaFetch(`/repos/${owner}/${repo}`);
  } catch {
    return null;
  }
}

/**
 * Delete a repo (used for project cleanup).
 * Treats 404 as success so deletes are idempotent if already removed.
 */
export async function deleteRepo(owner: string, repo: string): Promise<void> {
  try {
    await giteaFetch(`/repos/${owner}/${repo}`, { method: 'DELETE' });
  } catch (err) {
    if (err instanceof Error && /\b404\b/.test(err.message)) return;
    throw err;
  }
}

/**
 * Register a webhook on a repo that fires on push, PR, and issue events.
 *
 * @param owner      Repo owner (user or org)
 * @param repo       Repo name
 * @param webhookUrl Target URL — should include projectId as query param
 * @param secret     Shared secret for payload signature verification
 */
export async function createWebhook(
  owner: string,
  repo: string,
  webhookUrl: string,
  secret: string
): Promise<GiteaWebhook> {
  return giteaFetch(`/repos/${owner}/${repo}/hooks`, {
    method: 'POST',
    body: JSON.stringify({
      type: 'gitea',
      active: true,
      events: ['push', 'pull_request', 'issues', 'issue_comment'],
      config: {
        url: webhookUrl,
        content_type: 'json',
        secret,
      },
    }),
  });
}

/**
 * List webhooks on a repo.
 */
export async function listWebhooks(owner: string, repo: string): Promise<GiteaWebhook[]> {
  return giteaFetch(`/repos/${owner}/${repo}/hooks`);
}

/**
 * Delete a webhook.
 */
export async function deleteWebhook(owner: string, repo: string, hookId: number): Promise<void> {
  await giteaFetch(`/repos/${owner}/${repo}/hooks/${hookId}`, { method: 'DELETE' });
}

/**
 * Verify the X-Gitea-Signature-256 header on an incoming webhook payload.
 * Returns true if the signature matches.
 */
export async function verifyWebhookSignature(
  body: string,
  signature: string,
  secret: string
): Promise<boolean> {
  if (!signature?.startsWith('sha256=')) return false;

  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );

  const sigBytes = await crypto.subtle.sign('HMAC', key, encoder.encode(body));
  const expected = 'sha256=' + Array.from(new Uint8Array(sigBytes))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');

  return expected === signature;
}

/**
 * Push a single file to a repo via the Gitea contents API.
 * Creates the file if it doesn't exist; updates it if it does.
 */
export async function giteaPushFile(
  owner: string,
  repo: string,
  path: string,
  content: string,
  message: string,
  branch = 'main',
): Promise<void> {
  const encoded = Buffer.from(content).toString('base64');

  // Check if file already exists to get its SHA (required for updates)
  let sha: string | undefined;
  try {
    const existing = await giteaFetch(`/repos/${owner}/${repo}/contents/${path}?ref=${branch}`);
    sha = (existing as any)?.sha;
  } catch {
    // File doesn't exist — create it
  }

  await giteaFetch(`/repos/${owner}/${repo}/contents/${path}`, {
    method: sha ? 'PUT' : 'POST',
    body: JSON.stringify({ message, content: encoded, branch, ...(sha ? { sha } : {}) }),
  });
}

/**
 * Read a file from a Gitea repo. Returns decoded content + sha.
 * Throws if path is missing.
 */
export async function giteaReadFile(
  owner: string,
  repo: string,
  path: string,
  ref?: string,
): Promise<{ content: string; sha: string; size: number; encoding: string }> {
  const qs = ref ? `?ref=${encodeURIComponent(ref)}` : '';
  const data = await giteaFetch(`/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}${qs}`);
  const d = data as any;
  const raw = d.content && d.encoding === 'base64'
    ? Buffer.from(d.content, 'base64').toString('utf-8')
    : (d.content ?? '');
  return { content: raw, sha: d.sha, size: d.size ?? 0, encoding: d.encoding ?? 'utf-8' };
}

/** List files/folders at a path inside a Gitea repo. */
export async function giteaListContents(
  owner: string,
  repo: string,
  path = '',
  ref?: string,
): Promise<Array<{ name: string; path: string; type: string; size: number; sha: string }>> {
  const qs = ref ? `?ref=${encodeURIComponent(ref)}` : '';
  const data = await giteaFetch(`/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}${qs}`);
  const arr = Array.isArray(data) ? data : [data];
  return arr.map((d: any) => ({
    name: d.name,
    path: d.path,
    type: d.type,
    size: d.size ?? 0,
    sha: d.sha,
  }));
}

export interface GiteaBranch {
  name: string;
  commit: { id: string; message?: string };
  protected: boolean;
}

export async function giteaListBranches(owner: string, repo: string): Promise<GiteaBranch[]> {
  const data = await giteaFetch(`/repos/${owner}/${repo}/branches`);
  return Array.isArray(data) ? (data as GiteaBranch[]) : [];
}

/** Create a new branch from an existing one (defaults to repo's default branch). */
export async function giteaCreateBranch(
  owner: string,
  repo: string,
  newBranch: string,
  fromBranch?: string,
): Promise<GiteaBranch> {
  return giteaFetch(`/repos/${owner}/${repo}/branches`, {
    method: 'POST',
    body: JSON.stringify({
      new_branch_name: newBranch,
      ...(fromBranch ? { old_branch_name: fromBranch } : {}),
    }),
  }) as Promise<GiteaBranch>;
}

/** List repos owned by an org (or user). */
export async function giteaListOrgRepos(orgOrUser: string): Promise<GiteaRepo[]> {
  const data = await giteaFetch(`/orgs/${orgOrUser}/repos`).catch(async () => {
    // If it's not an org, try the user endpoint.
    return giteaFetch(`/users/${orgOrUser}/repos`);
  });
  return Array.isArray(data) ? (data as GiteaRepo[]) : [];
}

/**
 * Delete a single file at path. Requires the file's current sha.
 */
export async function giteaDeleteFile(
  owner: string,
  repo: string,
  path: string,
  sha: string,
  message: string,
  branch = 'main',
): Promise<void> {
  await giteaFetch(`/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, {
    method: 'DELETE',
    body: JSON.stringify({ message, sha, branch }),
  });
}

export const GITEA_ADMIN_USER_EXPORT = GITEA_ADMIN_USER;
