/**
 * SpaceMembersPanel.tsx — スペース「設定 → メンバー」パネル
 *
 * 共有スペース機能の管理 UI。現メンバー一覧（owner はバッジ付き・操作不可）と、
 * 招待ピッカー（active ユーザーから選んでロール指定で追加）を提供する。
 *
 * 管理操作（招待・ロール変更・除去）は「管理者」または「このスペースの owner」
 * のときだけ表示する。判定は以下の実シグナルで行う:
 *   - 認証無効(no-auth): synthetic local ユーザーが owner なので管理可。
 *   - 認証済み + role==='admin': 管理可。
 *   - 認証済み + 自分の id が owner メンバー行(isOwner)の userId と一致: 管理可。
 *   - それ以外: 読み取り専用。
 * 401/403 は mutation の onError でトースト表示してフェイルセーフにする。
 *
 * no-auth では /api/users/pickable が空配列を返すため、招待ピッカーの代わりに
 * 「認証を有効化するとスペースを共有できます」の案内を出す。
 */
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
  fetchSpaceMembers,
  fetchPickableUsers,
  addSpaceMember,
  updateSpaceMemberRole,
  removeSpaceMember,
  fetchSpaceInvite,
  createSpaceInvite,
  revokeSpaceInvite,
  type SpaceMember,
  type SpaceMemberRole,
  type SpaceInviteRole,
  type PickableUser,
} from '../../api';
import { useAuthState } from '../../App';

type ShowToast = (message: string, variant?: 'success' | 'error') => void;

function errMsg(e: unknown): string {
  return e instanceof Error ? e.message : String(e);
}

function Avatar({ url, name }: { url: string | null; name: string | null }) {
  const initial = (name ?? '?').trim().charAt(0).toUpperCase() || '?';
  if (url) {
    return <img src={url} alt="" className="h-8 w-8 shrink-0 rounded-full object-cover" />;
  }
  return (
    <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-2 text-xs font-semibold text-slate-600">
      {initial}
    </span>
  );
}

export function SpaceMembersPanel({ spaceId, showToast }: { spaceId: string; showToast?: ShowToast }) {
  const { t } = useTranslation('spaces');
  const auth = useAuthState();
  const qc = useQueryClient();

  const { data: members, isLoading, isError, error } = useQuery({
    queryKey: ['space-members', spaceId],
    queryFn: () => fetchSpaceMembers(spaceId),
    staleTime: 30_000,
  });

  // 管理可否（実シグナル）。owner メンバー行と自分の id を突き合わせる。
  const ownerRow = (members ?? []).find(m => m.isOwner);
  const canManage =
    auth.mode === 'disabled' ||
    (auth.mode === 'authenticated' &&
      (auth.user.role === 'admin' || (!!ownerRow && ownerRow.userId === auth.user.id)));

  const [picking, setPicking] = useState(false);

  const invalidate = () => qc.invalidateQueries({ queryKey: ['space-members', spaceId] });

  const roleMut = useMutation({
    mutationFn: ({ userId, role }: { userId: string; role: SpaceMemberRole }) =>
      updateSpaceMemberRole(spaceId, userId, role),
    onSuccess: invalidate,
    onError: (e) => showToast?.(t('members.toast.roleChangeFailed', { msg: errMsg(e) }), 'error'),
  });

  const removeMut = useMutation({
    mutationFn: (userId: string) => removeSpaceMember(spaceId, userId),
    onSuccess: invalidate,
    onError: (e) => showToast?.(t('members.toast.removeFailed', { msg: errMsg(e) }), 'error'),
  });

  const addMut = useMutation({
    mutationFn: (input: { userId: string; role: SpaceMemberRole }) => addSpaceMember(spaceId, input),
    onSuccess: () => { invalidate(); setPicking(false); },
    onError: (e) => showToast?.(t('members.toast.addFailed', { msg: errMsg(e) }), 'error'),
  });

  const handleRemove = (m: SpaceMember) => {
    const who = m.name ?? m.email ?? m.userId;
    if (window.confirm(t('members.removeConfirm', { who }))) {
      removeMut.mutate(m.userId);
    }
  };

  return (
    <div className="h-full overflow-y-auto" data-testid="space-members-panel">
      <div className="max-w-2xl mx-auto px-6 py-8 space-y-6">
        <div>
          <h2 className="text-base font-semibold text-slate-900 mb-1">{t('members.heading')}</h2>
          <p className="text-[13px] text-slate-500 leading-relaxed">
            {t('members.intro')}
          </p>
        </div>

        {isLoading && <div className="text-[13px] text-slate-400">{t('common:loading')}</div>}
        {isError && (
          <div className="text-[13px] text-red-600">{t('members.fetchError', { msg: errMsg(error) })}</div>
        )}

        {!isLoading && !isError && (
          <div>
            {(members ?? []).map(m => (
              <div
                key={m.userId}
                data-testid={`space-member-${m.userId}`}
                className="flex items-center gap-3 py-2.5 border-b border-hairline last:border-b-0"
              >
                <Avatar url={m.avatarUrl} name={m.name} />
                <div className="min-w-0 flex-1">
                  <div className="flex items-center gap-1.5">
                    <span className="text-[13px] font-medium text-slate-900 truncate">
                      {m.name ?? m.email ?? m.userId}
                    </span>
                    {m.isOwner && (
                      <span className="inline-block px-1.5 py-0.5 rounded text-[10px] font-medium bg-blue-50 dark:bg-blue-500/15 text-blue-600 dark:text-blue-300 leading-none">
                        {t('members.role.owner')}
                      </span>
                    )}
                  </div>
                  {m.email && <div className="text-2xs text-slate-500 truncate mt-0.5">{m.email}</div>}
                </div>

                <div className="flex items-center gap-2 flex-shrink-0">
                  {canManage && !m.isOwner ? (
                    <>
                      <select
                        data-testid={`space-member-role-${m.userId}`}
                        value={m.role}
                        disabled={roleMut.isPending}
                        onChange={(e) =>
                          roleMut.mutate({ userId: m.userId, role: e.target.value as SpaceMemberRole })
                        }
                        className="rounded border border-hairline px-2 py-1 text-xs bg-surface text-slate-700 focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent-ring"
                      >
                        <option value="editor">{t('members.role.editor')}</option>
                        <option value="viewer">{t('members.role.viewer')}</option>
                      </select>
                      <button
                        type="button"
                        data-testid={`space-member-remove-${m.userId}`}
                        onClick={() => handleRemove(m)}
                        disabled={removeMut.isPending}
                        className="text-2xs text-red-600 hover:text-red-800 dark:hover:text-red-300 underline disabled:opacity-50"
                      >
                        {t('members.remove')}
                      </button>
                    </>
                  ) : (
                    <span
                      data-testid={`space-member-role-${m.userId}`}
                      className="text-2xs text-slate-500"
                    >
                      {t(`members.role.${m.role}`)}
                    </span>
                  )}
                </div>
              </div>
            ))}
          </div>
        )}

        {/* 招待リンク（再利用トークン）。組織に依存せず、リンクを渡した相手が参加できる。 */}
        {canManage && auth.mode !== 'disabled' && (
          <InviteLinkSection spaceId={spaceId} showToast={showToast} />
        )}

        {/* 招待（ピッカー） */}
        {canManage && (
          <div>
            {picking ? (
              <InvitePicker
                spaceId={spaceId}
                existing={members ?? []}
                isPending={addMut.isPending}
                onCancel={() => setPicking(false)}
                onAdd={(userId, role) => addMut.mutate({ userId, role })}
              />
            ) : (
              <button
                type="button"
                data-testid="space-member-invite"
                onClick={() => setPicking(true)}
                className="px-3 py-1.5 rounded-md text-xs font-semibold text-accent border border-accent/30 hover:bg-accent-soft transition-colors"
              >
                {t('members.inviteButton')}
              </button>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

const EXPIRY_OPTIONS: Array<{ labelKey: string; days: number | null }> = [
  { labelKey: 'members.invite.expiry.never', days: null },
  { labelKey: 'members.invite.expiry.days7', days: 7 },
  { labelKey: 'members.invite.expiry.days30', days: 30 },
];

/**
 * 招待リンク・セクション。スペースごとに有効リンクは1本。役割・有効期限を選んで
 * 生成し、コピー / 再生成 / 無効化できる。リンクを知る相手だけが参加できるため、
 * 組織（pickable の絞り込み）に依存しない招待経路になる。
 */
function InviteLinkSection({ spaceId, showToast }: { spaceId: string; showToast?: ShowToast }) {
  const { t } = useTranslation('spaces');
  const qc = useQueryClient();
  const [role, setRole] = useState<SpaceInviteRole>('viewer');
  const [expiryIdx, setExpiryIdx] = useState(0);
  const [copied, setCopied] = useState(false);

  const { data: invite, isLoading } = useQuery({
    queryKey: ['space-invite', spaceId],
    queryFn: () => fetchSpaceInvite(spaceId),
    staleTime: 30_000,
  });

  const invalidate = () => qc.invalidateQueries({ queryKey: ['space-invite', spaceId] });

  const createMut = useMutation({
    mutationFn: () => createSpaceInvite(spaceId, { role, expiresInDays: EXPIRY_OPTIONS[expiryIdx].days }),
    onSuccess: invalidate,
    onError: (e) => showToast?.(t('members.toast.inviteCreateFailed', { msg: errMsg(e) }), 'error'),
  });

  const revokeMut = useMutation({
    mutationFn: () => revokeSpaceInvite(spaceId),
    onSuccess: invalidate,
    onError: (e) => showToast?.(t('members.toast.inviteRevokeFailed', { msg: errMsg(e) }), 'error'),
  });

  const absoluteUrl = invite ? `${window.location.origin}${invite.url}` : '';
  const active = !!invite && invite.valid;

  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(absoluteUrl);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch {
      showToast?.(t('members.toast.copyFailed'), 'error');
    }
  };

  return (
    <div data-testid="space-invite-section" className="rounded-md border border-hairline bg-surface/40 p-4 space-y-3 max-w-md">
      <div>
        <h3 className="text-[13px] font-semibold text-slate-900">{t('members.invite.title')}</h3>
        <p className="text-2xs text-slate-500 leading-relaxed mt-0.5">
          {t('members.invite.description')}
        </p>
      </div>

      {isLoading ? (
        <div className="text-[13px] text-slate-400">{t('common:loading')}</div>
      ) : active ? (
        <div className="space-y-2">
          <div className="flex items-center gap-2">
            <input
              readOnly
              data-testid="space-invite-url"
              value={absoluteUrl}
              onFocus={(e) => e.currentTarget.select()}
              className="h-8 flex-1 rounded-md border border-hairline bg-canvas px-2 text-2xs text-slate-700 focus:border-accent focus:outline-none"
            />
            <button
              type="button"
              data-testid="space-invite-copy"
              onClick={handleCopy}
              className="h-8 shrink-0 rounded-md bg-accent px-3 text-xs font-semibold text-accent-fg hover:bg-accent-deep"
            >
              {copied ? t('members.invite.copied') : t('members.invite.copy')}
            </button>
          </div>
          <div className="flex items-center justify-between text-2xs text-slate-500">
            <span>
              {t('members.invite.roleLabel', { role: t(`members.role.${invite.role}`) })}
              {invite.expiresAt
                ? t('members.invite.expiresAt', { date: new Date(invite.expiresAt.replace(' ', 'T') + 'Z').toLocaleDateString() })
                : t('members.invite.noExpiry')}
            </span>
            <div className="flex items-center gap-2">
              <button
                type="button"
                data-testid="space-invite-regenerate"
                onClick={() => createMut.mutate()}
                disabled={createMut.isPending}
                className="text-slate-600 underline hover:text-slate-900 disabled:opacity-50"
              >
                {t('members.invite.regenerate')}
              </button>
              <button
                type="button"
                data-testid="space-invite-revoke"
                onClick={() => revokeMut.mutate()}
                disabled={revokeMut.isPending}
                className="text-red-600 underline hover:text-red-800 disabled:opacity-50"
              >
                {t('members.invite.revoke')}
              </button>
            </div>
          </div>
        </div>
      ) : (
        <div className="space-y-2">
          <div className="flex items-center gap-2">
            <select
              data-testid="space-invite-role"
              value={role}
              onChange={(e) => setRole(e.target.value as SpaceInviteRole)}
              className="rounded border border-hairline px-2 py-1 text-xs bg-surface text-slate-700 focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent-ring"
            >
              <option value="viewer">{t('members.role.viewer')}</option>
              <option value="editor">{t('members.role.editor')}</option>
            </select>
            <select
              data-testid="space-invite-expiry"
              value={expiryIdx}
              onChange={(e) => setExpiryIdx(Number(e.target.value))}
              className="rounded border border-hairline px-2 py-1 text-xs bg-surface text-slate-700 focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent-ring"
            >
              {EXPIRY_OPTIONS.map((o, i) => (
                <option key={i} value={i}>{t(o.labelKey)}</option>
              ))}
            </select>
            <button
              type="button"
              data-testid="space-invite-create"
              onClick={() => createMut.mutate()}
              disabled={createMut.isPending}
              className="rounded-md bg-accent px-3 py-1.5 text-xs font-semibold text-accent-fg hover:bg-accent-deep disabled:opacity-50"
            >
              {createMut.isPending ? t('members.invite.creating') : t('members.invite.create')}
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

function InvitePicker({
  spaceId,
  existing,
  isPending,
  onCancel,
  onAdd,
}: {
  spaceId: string;
  existing: SpaceMember[];
  isPending: boolean;
  onCancel: () => void;
  onAdd: (userId: string, role: SpaceMemberRole) => void;
}) {
  const { t } = useTranslation('spaces');
  const { data: users, isLoading } = useQuery({
    queryKey: ['pickable-users', spaceId],
    queryFn: fetchPickableUsers,
    staleTime: 30_000,
  });

  const [search, setSearch] = useState('');
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [role, setRole] = useState<SpaceMemberRole>('editor');

  // 既にメンバー / owner のユーザーは候補から除外。
  const memberIds = useMemo(() => new Set(existing.map(m => m.userId)), [existing]);
  const candidates = useMemo(() => {
    const q = search.trim().toLowerCase();
    return (users ?? [])
      .filter(u => !memberIds.has(u.id))
      .filter(u => {
        if (!q) return true;
        return (
          (u.name ?? '').toLowerCase().includes(q) ||
          (u.email ?? '').toLowerCase().includes(q)
        );
      });
  }, [users, memberIds, search]);

  // no-auth: pickable が空 = 実ユーザー不在。共有できない案内を出す。
  if (!isLoading && (users ?? []).length === 0) {
    return (
      <div
        data-testid="space-member-picker"
        className="rounded-md border border-hairline bg-surface/50 p-4 text-[13px] text-slate-500"
      >
        {t('members.picker.authRequired')}
        <div className="mt-3">
          <button
            type="button"
            onClick={onCancel}
            className="text-xs text-slate-600 hover:text-slate-800 underline"
          >
            {t('members.picker.close')}
          </button>
        </div>
      </div>
    );
  }

  // 候補ゼロの理由を区別する: 全員追加済みか、そもそも同じ組織にメンバーがいないか。
  const allAlreadyAdded = (users ?? []).length > 0 && candidates.length === 0 && search.trim() === '';

  return (
    <div
      data-testid="space-member-picker"
      className="rounded-md border border-hairline bg-surface/50 p-4 space-y-3 max-w-md"
    >
      <p data-testid="space-member-picker-org-note" className="text-2xs text-slate-500 leading-relaxed">
        {t('members.picker.orgNote')}
      </p>
      <input
        autoFocus
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        placeholder={t('members.picker.searchPlaceholder')}
        className="h-8 w-full rounded-md border border-hairline px-2 text-xs focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent-ring"
      />

      {isLoading ? (
        <div className="text-[13px] text-slate-400">{t('common:loading')}</div>
      ) : candidates.length === 0 ? (
        <div className="text-[13px] text-slate-400">
          {allAlreadyAdded
            ? t('members.picker.allAdded')
            : t('members.picker.noCandidates')}
        </div>
      ) : (
        <div className="max-h-56 overflow-y-auto">
          {candidates.map((u: PickableUser) => {
            const active = selectedId === u.id;
            return (
              <button
                key={u.id}
                type="button"
                onClick={() => setSelectedId(u.id)}
                className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left transition-colors ${
                  active ? 'bg-accent-soft' : 'hover:bg-surface'
                }`}
              >
                <Avatar url={u.avatarUrl} name={u.name} />
                <div className="min-w-0 flex-1">
                  <div className="text-[13px] text-slate-900 truncate">{u.name ?? u.email ?? u.id}</div>
                  {u.email && <div className="text-2xs text-slate-500 truncate">{u.email}</div>}
                </div>
              </button>
            );
          })}
        </div>
      )}

      <div className="flex items-center gap-2">
        <select
          value={role}
          onChange={(e) => setRole(e.target.value as SpaceMemberRole)}
          className="rounded border border-hairline px-2 py-1 text-xs bg-surface text-slate-700 focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent-ring"
        >
          <option value="editor">{t('members.role.editor')}</option>
          <option value="viewer">{t('members.role.viewer')}</option>
        </select>
        <button
          type="button"
          disabled={!selectedId || isPending}
          onClick={() => selectedId && onAdd(selectedId, role)}
          className="px-4 py-1.5 rounded-md text-xs font-semibold bg-accent text-accent-fg hover:bg-accent-deep transition-colors disabled:opacity-50"
        >
          {isPending ? t('members.picker.adding') : t('members.picker.add')}
        </button>
        <button
          type="button"
          onClick={onCancel}
          className="px-4 py-1.5 rounded-md text-xs text-slate-700 border border-hairline hover:bg-surface transition-colors"
        >
          {t('common:cancel')}
        </button>
      </div>
    </div>
  );
}
