import type { ServiceConnection } from '../../hooks/useServiceConnection';
import { useEffect, useState } from 'react';
import { CheckCircle2, ExternalLink, Loader2, Plug, Trash2, TriangleAlert } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { serviceErrorMessage, useServiceConnectionActions, useServiceHealth } from '../../hooks/useServiceConnection';
import { isUnreachableFromBrowser } from './shared';
import { useGiteaConnection } from './useGiteaData';

// Connecting a Gitea instance to Officer, from the app.
//
// Both the setup wizard and the permanent settings page: GiteaView renders it in place of whatever section
// the nav asked for while nothing is connected, and the Connection section renders it for good. One
// component, so re-pointing at a different instance later goes through exactly the code path that stored
// the first one.
//
// The token is write-only — the GET it reads has no field that could carry one back, so the input is always
// blank and an empty input means "keep the stored token". That is what lets the URL be corrected without
// the token being re-pasted.
//
// TWO FORMS, not one. There is one instance and a token per person: the server owner names the instance,
// everyone else supplies only their own token against it. The sidecar enforces that — a `url` from a member
// is answered 403, not ignored — so this must not send one. The rule is upstream of the UI on purpose; what
// happens here is only that a field nobody is allowed to fill in is not drawn.

const HINT = 'text-xs leading-relaxed text-muted-foreground';

/** Gitea's own default HTTP port. Officer dials it from the server, not from this browser. */
const DEFAULT_URL = 'http://localhost:3000';

const URL_HINT =
  "The instance's base URL, without /api. Officer reaches it from the server, not from this browser — so " +
  'localhost here means the machine Officer runs on.';

const MEMBER_URL_HINT =
  'The instance this server is connected to. Only the server owner can change it — your half of this is the token.';

const TOKEN_HINT =
  'A personal access token from the instance: Settings → Applications → Generate Token. Officer sends it ' +
  'as `Authorization: token …` on every call. Scopes read:user and read:repository are enough to browse; ' +
  'grant write only if you want Officer to change things.';

type SaveInput = Record<string, unknown> & { token: string };

export const GiteaConnection = () => {
  const { data, isLoading } = useGiteaConnection();
  const { data: health } = useServiceHealth('gitea');
  const { save, forget } = useServiceConnectionActions<SaveInput>('gitea');

  const connection = data?.connection ?? null;
  // Defaulted to OWNER deliberately, and it is not a loading concern — the `isLoading` guard below means
  // the form never renders before `data` arrives. The only way past it without data is a FAILED query
  // (`retry: false`), i.e. the sidecar is down — and that is exactly when the owner needs the URL field to
  // repoint a broken instance. Defaulting to false would hide it at the one moment it is needed. A member
  // seeing the field in that state costs nothing: the sidecar rejects a `url` from them regardless.
  const isOwner = data?.isOwner ?? true;
  const instanceUrl = data?.instanceUrl ?? null;

  const [url, setUrl] = useState('');
  const [token, setToken] = useState('');
  const [error, setError] = useState('');

  // Seed from the stored row once it arrives. Keyed on its id so a save (same id) doesn't stomp what the
  // owner is still typing, while forgetting and re-adding does reset the form.
  useEffect(() => {
    setUrl(connection?.url ?? '');
    setToken('');
  }, [connection?.id]);

  const submit = async () => {
    setError('');
    try {
      // A member sends no `url` KEY at all — the sidecar rejects the field's presence, not just a value,
      // so `url: undefined` would still have to survive JSON.stringify dropping it and is not worth relying on.
      await save.mutateAsync(isOwner ? { url: url.trim(), token: token.trim() } : { token: token.trim() });
      setToken('');
    } catch (err) {
      setError(serviceErrorMessage(err));
    }
  };

  const remove = async () => {
    setError('');
    try {
      await forget.mutateAsync();
    } catch (err) {
      setError(serviceErrorMessage(err));
    }
  };

  if (isLoading) {
    return (
      <div className="flex h-full items-center justify-center text-sm text-muted-foreground">
        <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Loading…
      </div>
    );
  }

  // Where to mint a token: the URL being typed while the owner is setting the instance up, the instance
  // that is already there for everyone else.
  const base = (isOwner ? url.trim() : (instanceUrl ?? '')).replace(/\/+$/, '');
  const tokenUrl = base ? `${base}/user/settings/applications` : null;
  // A member cannot connect before there is an instance to connect to, and saying so beats a 409.
  const awaitingInstance = !isOwner && !data?.instanceConfigured;

  // Officer dials the instance from the server, but avatars and "open in Gitea" links are fetched by the
  // BROWSER, straight from whatever origin the instance mints them on. A loopback or LAN address works for
  // the first and not the second, and the symptom — broken avatars, dead links — looks nothing like its
  // cause, so it is worth saying out loud rather than leaving to be discovered.
  const dialOnly = (() => {
    const stored = connection?.url ?? instanceUrl;
    if (!stored) return false;
    try {
      return isUnreachableFromBrowser(new URL(stored).hostname);
    } catch {
      return false;
    }
  })();

  return (
    <div className="h-full overflow-y-auto">
      <div className="mx-auto flex max-w-xl flex-col gap-6 p-6">
        <header className="flex items-start gap-3">
          <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-success/15 text-success">
            <Plug className="h-5 w-5" />
          </div>
          <div>
            <h2 className="text-sm font-semibold">{isOwner ? 'Gitea instance' : 'Your Gitea account'}</h2>
            <p className={HINT}>
              {!isOwner
                ? awaitingInstance
                  ? 'No Gitea instance has been connected on this server yet. Once the owner adds one, sign in to it here with your own access token.'
                  : 'Sign in to this server’s Gitea with your own access token. What you see is your account — your repositories, your notifications.'
                : connection
                  ? 'Where Officer talks to Gitea. Saving re-checks the instance before storing anything.'
                  : 'Point Officer at your Gitea instance. It needs the URL and one access token.'}
            </p>
          </div>
        </header>

        {connection && <StatusRow connection={connection} instanceUrl={instanceUrl} health={health} />}

        {connection && dialOnly && (
          <div className="flex items-start gap-2 rounded-xl border border-warning/40 bg-warning/10 p-3 text-xs">
            <TriangleAlert className="mt-px h-4 w-4 shrink-0 text-warning" />
            <div className="min-w-0">
              <div className="font-medium">Avatars and links will not load</div>
              <p className="mt-0.5 text-muted-foreground">
                This address only works from the server. Officer can talk to Gitea, but your browser loads avatars and
                “open in Gitea” links directly from the instance, so they need an address this browser can reach too.{' '}
                {isOwner
                  ? 'Use the instance’s public URL here instead.'
                  : 'Ask the server owner to use the instance’s public URL.'}
              </p>
            </div>
          </div>
        )}

        <div className="flex flex-col gap-4 rounded-xl border p-4">
          {isOwner ? (
            <label className="flex flex-col gap-1.5">
              <span className="text-xs font-medium">Instance URL</span>
              <Input
                value={url}
                onChange={(ev) => setUrl(ev.target.value)}
                placeholder={DEFAULT_URL}
                autoFocus={!connection}
                autoComplete="off"
                spellCheck={false}
              />
              <span className={HINT}>{URL_HINT}</span>
            </label>
          ) : (
            <div className="flex flex-col gap-1.5">
              <span className="text-xs font-medium">Instance</span>
              <div className="truncate rounded-md border bg-muted/40 px-3 py-2 text-sm text-muted-foreground">
                {instanceUrl ?? 'Not connected yet'}
              </div>
              <span className={HINT}>{MEMBER_URL_HINT}</span>
            </div>
          )}

          <label className="flex flex-col gap-1.5">
            <span className="text-xs font-medium">Access token</span>
            <Input
              value={token}
              onChange={(ev) => setToken(ev.target.value)}
              placeholder={connection?.hasSecret ? '•••••••• (unchanged)' : 'Gitea personal access token'}
              type="password"
              autoComplete="off"
              spellCheck={false}
            />
            <span className={HINT}>
              {TOKEN_HINT}{' '}
              {tokenUrl && (
                <a
                  href={tokenUrl}
                  target="_blank"
                  rel="noreferrer"
                  className="inline-flex items-center gap-0.5 font-medium text-primary hover:underline"
                >
                  Open token settings
                  <ExternalLink className="h-2.5 w-2.5" />
                </a>
              )}
            </span>
          </label>

          {error && (
            <div className="flex items-start gap-2 rounded-lg bg-destructive/10 p-2.5 text-xs text-destructive">
              <TriangleAlert className="mt-px h-3.5 w-3.5 shrink-0" />
              <span>{error}</span>
            </div>
          )}

          <div className="flex items-center gap-2">
            <Button
              size="sm"
              onClick={submit}
              disabled={
                (isOwner ? !url.trim() : awaitingInstance) ||
                (!token.trim() && !connection?.hasSecret) ||
                save.isPending
              }
            >
              {save.isPending && <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />}
              {connection ? 'Save' : 'Connect'}
            </Button>
            {connection && (
              <Button size="sm" variant="ghost" onClick={remove} disabled={forget.isPending}>
                <Trash2 className="mr-2 h-3.5 w-3.5" />
                Disconnect
              </Button>
            )}
          </div>
        </div>
      </div>
    </div>
  );
};

type StatusRowProps = {
  connection: ServiceConnection;
  /** The resolved instance, which is where a member's row gets its base from — theirs stores none. */
  instanceUrl: string | null;
  health: { ok: boolean; error?: string } | undefined;
};

const StatusRow = ({ connection, instanceUrl, health }: StatusRowProps) => (
  <div className="flex items-start gap-2 rounded-xl border p-3 text-xs">
    {health?.ok ? (
      <CheckCircle2 className="mt-px h-4 w-4 shrink-0 text-success" />
    ) : (
      <TriangleAlert className="mt-px h-4 w-4 shrink-0 text-warning" />
    )}
    <div className="min-w-0">
      <div className="font-medium">{health?.ok ? 'Connected' : 'Not responding'}</div>
      <div className="truncate text-muted-foreground">
        {connection.url ?? instanceUrl ?? '—'}
        {connection.version ? ` · Gitea ${connection.version}` : ''}
      </div>
      {!health?.ok && health?.error && <div className="text-destructive">{health.error}</div>}
    </div>
  </div>
);
