import type {
  GiteaBranch,
  GiteaChangedFile,
  GiteaComment,
  GiteaCommit,
  GiteaContentsEntry,
  GiteaIssue,
  GiteaLabel,
  GiteaMilestone,
  GiteaNotification,
  GiteaOrg,
  GiteaPullRequest,
  GiteaRelease,
  GiteaRepo,
  GiteaSearchResult,
  GiteaTag,
  GiteaUser,
} from './shared';
import type { ServiceConnectionState } from '../../hooks/useServiceConnection';
import { useQuery } from '@tanstack/react-query';
import { useClient } from 'hooks/useClient';
import { useServiceConnection } from '../../hooks/useServiceConnection';
import { retargetUrls } from './shared';

// Reads against the instance, through the sidecar's `/_api/*` pass-through.
//
// The connection itself is NOT handled here — it uses the shared `useServiceConnection` /
// `useServiceHealth` / `useServiceConnectionActions` hooks, because gitea serves the same
// single-connection `_config` contract as slskd and transmission. This file is only the app data.
//
// Note the doubled prefix in the paths: `useClient` is based at `/api`, the platform mounts the proxy at
// `/api/gitea` and strips it, and the sidecar strips its own `/_api` before forwarding — so
// `/gitea/_api/api/v1/user/repos` arrives at the instance as `/api/v1/user/repos`. It reads oddly and it
// is correct; the alternative is the sidecar guessing which paths are upstream's.

const KEY = 'gitea';

/** `/api/v1` under the sidecar's pass-through, with each path segment escaped exactly once. */
const api = (path: string) => `/gitea/_api/api/v1${path}`;

/**
 * A repo path segment. Owner and repo names cannot contain a slash, but they CAN contain characters that
 * matter in a URL (a dot, a plus), so they go through encodeURIComponent — while a *file* path must keep
 * its slashes and uses `encodePath` below.
 */
const repoBase = (owner: string, repo: string) => `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;

/** Escape a file path segment-wise, so `src/a b.ts` survives without its separators being eaten. */
const encodePath = (path: string) => path.split('/').map(encodeURIComponent).join('/');

/**
 * `enabled` is how callers gate on there being a connection at all. Unconfigured, the sidecar answers 503
 * to everything under `/_api`, so firing these anyway would put a failed request behind a form the owner
 * has not filled in yet — and React Query would surface it as an error state on the setup screen.
 */
type Options = { enabled?: boolean };

/**
 * Gitea's `_config` answers with more than the two common fields, because it is the first sidecar where the
 * connection is not one thing: the instance belongs to the server owner and the token belongs to whoever is
 * asking. `connection.url` is therefore NULL for everyone but the owner — their row is a bare credential —
 * so `instanceUrl` is the resolved base, and `isOwner` decides whether the form offers a URL field at all.
 */
export type GiteaConnectionState = ServiceConnectionState & {
  instanceUrl: string | null;
  instanceConfigured: boolean;
  isOwner: boolean;
};

export const useGiteaConnection = () => useServiceConnection<GiteaConnectionState>('gitea');

/**
 * `get`, with every instance-minted URL in the response rebased onto the connection's own URL.
 *
 * It lives in the data layer rather than at each `<img>` and `<a>` because it is not a rendering choice —
 * a response whose `avatar_url` says `http://localhost:9004` is simply wrong for this client, and fixing it
 * once on arrival is what stops every view from having to remember. See `retargetUrls` for why the
 * instance answers that way at all.
 */
function useGiteaGet(): <T>(path: string) => Promise<T> {
  const { get } = useClient();
  const { data } = useGiteaConnection();
  // `instanceUrl`, not `connection.url` — the latter is null for anyone but the owner, and a member's
  // avatars are just as wrong as the owner's.
  const origin = data?.instanceUrl ?? data?.connection?.url ?? '';
  return async <T>(path: string) => retargetUrls(await get<T>(path), origin);
}

// Two staleness tiers, and the split is deliberate. Things keyed by an immutable git object (a commit, a
// blob at a sha) never change, so re-fetching them is pure waste; things keyed by a moving target (a branch
// tip, an issue list) go stale the moment somebody pushes.
const LIVE = 60_000;
const STATIC = 300_000;

// ── Account ───────────────────────────────────────────────────────────────────────────────────────

/** The account the stored token belongs to. */
export function useGiteaViewer({ enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'viewer'],
    queryFn: () => get<GiteaUser>(api('/user')),
    staleTime: STATIC,
    retry: false,
    enabled,
  });
}

/**
 * Repositories the token can see, most recently touched first.
 *
 * `/user/repos` rather than `/repos/search`: it returns what this token actually has access to, including
 * private repos and those owned by orgs the account belongs to, without a query to get wrong. Gitea pages
 * at 30 by default; 50 is one screenful of scrolling and still one request.
 */
export function useGiteaRepos({ enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'repos'],
    queryFn: () => get<GiteaRepo[]>(api('/user/repos?limit=50&page=1')),
    staleTime: LIVE,
    retry: false,
    enabled,
  });
}

export function useGiteaOrgs({ enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'orgs'],
    queryFn: () => get<GiteaOrg[]>(api('/user/orgs?limit=50')),
    staleTime: STATIC,
    retry: false,
    enabled,
  });
}

/**
 * Unread notifications. Gitea's own UI counts these in the header, and the parameter name really is
 * hyphenated (`status-types`), repeated once per status wanted.
 */
export function useGiteaNotifications({ enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'notifications'],
    queryFn: () => get<GiteaNotification[]>(api('/notifications?status-types=unread&limit=50')),
    staleTime: 30_000,
    retry: false,
    enabled,
  });
}

// ── Cross-repository search ───────────────────────────────────────────────────────────────────────

/** Instance-wide repository search. An empty query lists what the token can see, which is the Explore page. */
export function useGiteaRepoSearch(query: string, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'repo-search', query],
    queryFn: () => get<GiteaSearchResult<GiteaRepo>>(api(`/repos/search?q=${encodeURIComponent(query)}&limit=50`)),
    staleTime: LIVE,
    retry: false,
    enabled,
  });
}

type IssueSearchOptions = Options & { state?: 'open' | 'closed' | 'all'; type?: 'issues' | 'pulls' };

/**
 * Issues and pull requests across every repository the token can see — the "Issues" / "Pull Requests"
 * dashboards in Gitea's own header. `type` is what separates the two: in Gitea a pull request IS an issue,
 * and the list endpoint returns both unless told otherwise.
 */
export function useGiteaIssueSearch({ state = 'open', type = 'issues', enabled = true }: IssueSearchOptions = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'issue-search', state, type],
    queryFn: () => get<GiteaIssue[]>(api(`/repos/issues/search?state=${state}&type=${type}&limit=50`)),
    staleTime: LIVE,
    retry: false,
    enabled,
  });
}

// ── One repository ────────────────────────────────────────────────────────────────────────────────

type RepoRef = { owner: string; repo: string };

export function useGiteaRepo({ owner, repo }: RepoRef, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'repo', owner, repo],
    queryFn: () => get<GiteaRepo>(api(repoBase(owner, repo))),
    staleTime: LIVE,
    retry: false,
    enabled: enabled && !!owner && !!repo,
  });
}

export function useGiteaBranches({ owner, repo }: RepoRef, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'branches', owner, repo],
    queryFn: () => get<GiteaBranch[]>(api(`${repoBase(owner, repo)}/branches?limit=100`)),
    staleTime: LIVE,
    retry: false,
    enabled: enabled && !!owner && !!repo,
  });
}

export function useGiteaTags({ owner, repo }: RepoRef, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'tags', owner, repo],
    queryFn: () => get<GiteaTag[]>(api(`${repoBase(owner, repo)}/tags?limit=100`)),
    staleTime: LIVE,
    retry: false,
    enabled: enabled && !!owner && !!repo,
  });
}

type ContentsOptions = Options & { path?: string; ref?: string };

/**
 * A directory listing or a single file, depending on what `path` points at — Gitea answers an array for the
 * former and a bare object for the latter, off the same URL. Callers discriminate with `Array.isArray`.
 *
 * The empty path is the repository root, and it must NOT get a trailing slash: `/contents/` 404s on some
 * versions where `/contents` does not.
 */
export function useGiteaContents({ owner, repo }: RepoRef, { path = '', ref, enabled = true }: ContentsOptions = {}) {
  const get = useGiteaGet();
  const suffix = path ? `/${encodePath(path)}` : '';
  const query = ref ? `?ref=${encodeURIComponent(ref)}` : '';
  return useQuery({
    queryKey: [KEY, 'contents', owner, repo, path, ref ?? ''],
    queryFn: () =>
      get<GiteaContentsEntry | GiteaContentsEntry[]>(api(`${repoBase(owner, repo)}/contents${suffix}${query}`)),
    staleTime: LIVE,
    retry: false,
    enabled: enabled && !!owner && !!repo,
  });
}

type CommitsOptions = Options & { ref?: string; path?: string; page?: number };

export function useGiteaCommits(
  { owner, repo }: RepoRef,
  { ref, path, page = 1, enabled = true }: CommitsOptions = {},
) {
  const get = useGiteaGet();
  const query = new URLSearchParams({ limit: '30', page: String(page) });
  if (ref) query.set('sha', ref);
  if (path) query.set('path', path);
  return useQuery({
    queryKey: [KEY, 'commits', owner, repo, ref ?? '', path ?? '', page],
    queryFn: () => get<GiteaCommit[]>(api(`${repoBase(owner, repo)}/commits?${query}`)),
    staleTime: LIVE,
    retry: false,
    enabled: enabled && !!owner && !!repo,
  });
}

/** One commit, including the per-file stats Gitea only fills in on the single-commit endpoint. */
export function useGiteaCommit({ owner, repo }: RepoRef, sha: string, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'commit', owner, repo, sha],
    queryFn: () => get<GiteaCommit>(api(`${repoBase(owner, repo)}/git/commits/${encodeURIComponent(sha)}`)),
    // A commit is immutable, so once fetched it never needs re-fetching for the life of the tab.
    staleTime: Infinity,
    retry: false,
    enabled: enabled && !!owner && !!repo && !!sha,
  });
}

export function useGiteaReleases({ owner, repo }: RepoRef, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'releases', owner, repo],
    queryFn: () => get<GiteaRelease[]>(api(`${repoBase(owner, repo)}/releases?limit=30`)),
    staleTime: LIVE,
    retry: false,
    enabled: enabled && !!owner && !!repo,
  });
}

export function useGiteaLabels({ owner, repo }: RepoRef, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'labels', owner, repo],
    queryFn: () => get<GiteaLabel[]>(api(`${repoBase(owner, repo)}/labels?limit=100`)),
    staleTime: STATIC,
    retry: false,
    enabled: enabled && !!owner && !!repo,
  });
}

export function useGiteaMilestones({ owner, repo }: RepoRef, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'milestones', owner, repo],
    queryFn: () => get<GiteaMilestone[]>(api(`${repoBase(owner, repo)}/milestones?state=all&limit=50`)),
    staleTime: STATIC,
    retry: false,
    enabled: enabled && !!owner && !!repo,
  });
}

// ── Issues and pull requests ──────────────────────────────────────────────────────────────────────

type RepoIssuesOptions = Options & { state?: 'open' | 'closed' | 'all'; type?: 'issues' | 'pulls'; labels?: string };

export function useGiteaRepoIssues(
  { owner, repo }: RepoRef,
  { state = 'open', type = 'issues', labels, enabled = true }: RepoIssuesOptions = {},
) {
  const get = useGiteaGet();
  const query = new URLSearchParams({ state, type, limit: '50' });
  if (labels) query.set('labels', labels);
  return useQuery({
    queryKey: [KEY, 'repo-issues', owner, repo, state, type, labels ?? ''],
    queryFn: () => get<GiteaIssue[]>(api(`${repoBase(owner, repo)}/issues?${query}`)),
    staleTime: LIVE,
    retry: false,
    enabled: enabled && !!owner && !!repo,
  });
}

/**
 * One issue by its per-repository number.
 *
 * Gitea serves a pull request from this endpoint too — a PR and an issue share a number space — so an issue
 * detail screen works for both, and only the extra PR-specific data needs `/pulls/{index}`.
 */
export function useGiteaIssue({ owner, repo }: RepoRef, index: number, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'issue', owner, repo, index],
    queryFn: () => get<GiteaIssue>(api(`${repoBase(owner, repo)}/issues/${index}`)),
    staleTime: LIVE,
    retry: false,
    enabled: enabled && !!owner && !!repo && Number.isFinite(index),
  });
}

export function useGiteaIssueComments({ owner, repo }: RepoRef, index: number, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'issue-comments', owner, repo, index],
    queryFn: () => get<GiteaComment[]>(api(`${repoBase(owner, repo)}/issues/${index}/comments`)),
    staleTime: LIVE,
    retry: false,
    enabled: enabled && !!owner && !!repo && Number.isFinite(index),
  });
}

export function useGiteaPull({ owner, repo }: RepoRef, index: number, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'pull', owner, repo, index],
    queryFn: () => get<GiteaPullRequest>(api(`${repoBase(owner, repo)}/pulls/${index}`)),
    staleTime: LIVE,
    retry: false,
    enabled: enabled && !!owner && !!repo && Number.isFinite(index),
  });
}

/** The changed-file summary for a pull request — filename plus +/- counts, not the patch text. */
export function useGiteaPullFiles({ owner, repo }: RepoRef, index: number, { enabled = true }: Options = {}) {
  const get = useGiteaGet();
  return useQuery({
    queryKey: [KEY, 'pull-files', owner, repo, index],
    queryFn: () => get<GiteaChangedFile[]>(api(`${repoBase(owner, repo)}/pulls/${index}/files?limit=100`)),
    staleTime: LIVE,
    retry: false,
    enabled: enabled && !!owner && !!repo && Number.isFinite(index),
  });
}
