import { useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
  fetchSpaceFiles,
  fetchSpaceFileContent,
  getAppShareLink,
  createAppShareLink,
  revokeAppShareLink,
} from '../../api';
import { deriveAppList, type WorkspaceApp } from './app-bridge';
import { AppRunner } from './AppRunner';
import { createSpaceGateway } from './app-file-gateway';
import { buildAppShareDisplayUrl } from './appShareUrl';

/**
 * SpaceApps — the workspace "アプリ" tab.
 *
 * Lists the runnable workspace apps (subfolders under `apps/` that contain an
 * `index.html`) and launches a selected one in the AppRunner (sandboxed iframe
 * + postMessage bridge from Stage 1). Each app may carry an optional
 * `apps/{name}/app.json` manifest ({title?, description?, entry?}); when present
 * its title/description are shown, otherwise we fall back to the folder name +
 * `index.html`.
 *
 * NETWORK / SECURITY: the apps themselves run under AppRunner's opaque-origin
 * sandbox with `connect-src 'none'` — this tab only discovers + launches them.
 */
export function SpaceApps({ spaceId, canManage = true }: { spaceId: string; canManage?: boolean }) {
  const { t } = useTranslation('spaces');
  const [appToRun, setAppToRun] = useState<WorkspaceApp | null>(null);

  const appsQuery = useQuery({
    queryKey: ['space-apps', spaceId],
    queryFn: () => loadWorkspaceApps(spaceId),
    staleTime: 10_000,
  });

  const apps = appsQuery.data ?? [];

  return (
    <div data-testid="space-apps" className="flex flex-col gap-3 p-4">
      <div className="flex items-center justify-between gap-2">
        <p className="text-[11px] font-bold uppercase tracking-wider text-slate-500">
          {t('apps.heading')}
        </p>
        <button
          type="button"
          data-testid="space-apps-refresh"
          onClick={() => void appsQuery.refetch()}
          disabled={appsQuery.isFetching}
          title={t('apps.refresh')}
          aria-label={t('apps.refresh')}
          className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-hairline bg-canvas text-slate-500 transition-colors hover:bg-surface hover:text-slate-900 disabled:opacity-50"
        >
          <svg className={`h-3.5 w-3.5 ${appsQuery.isFetching ? 'animate-spin' : ''}`} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
            <path d="M2 8a6 6 0 0110.5-4M14 8a6 6 0 01-10.5 4" />
            <path d="M12 2v3h-3M4 14v-3h3" />
          </svg>
        </button>
      </div>

      {appsQuery.isError && (
        <p className="text-xs text-red-600">{t('apps.fetchError')}</p>
      )}

      {!appsQuery.isLoading && !appsQuery.isError && apps.length === 0 && (
        <div
          data-testid="space-apps-empty"
          className="rounded-lg border border-dashed border-hairline bg-surface p-6 text-center text-sm text-slate-500"
        >
          <p className="font-medium text-slate-600">{t('apps.empty.title')}</p>
          <p className="mt-1.5 leading-relaxed">
            {t('apps.empty.body')}
          </p>
          <p className="mt-1.5 text-2xs text-slate-400">
            <Trans
              t={t}
              i18nKey="apps.empty.location"
              components={{ code: <code className="rounded bg-canvas px-1 py-0.5" /> }}
            />
          </p>
        </div>
      )}

      {apps.length > 0 && (
        <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
          {apps.map(app => (
            <div
              key={app.name}
              data-testid={`space-app-${app.name}`}
              className="flex flex-col gap-2 rounded-lg border border-hairline bg-surface p-3"
            >
              <div className="flex min-w-0 items-start gap-2">
                <span className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-[var(--brand-primary)]/10 text-[var(--brand-primary)]" aria-hidden>
                  <svg className="h-4 w-4" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                    <rect x="2" y="2" width="5" height="5" rx="1" />
                    <rect x="9" y="2" width="5" height="5" rx="1" />
                    <rect x="2" y="9" width="5" height="5" rx="1" />
                    <rect x="9" y="9" width="5" height="5" rx="1" />
                  </svg>
                </span>
                <div className="min-w-0 flex-1">
                  <p className="truncate text-sm font-semibold text-slate-800" title={app.title}>
                    {app.title}
                  </p>
                  {app.description ? (
                    <p className="mt-0.5 line-clamp-2 text-2xs text-slate-500">{app.description}</p>
                  ) : (
                    <p className="mt-0.5 truncate font-mono text-2xs text-slate-400">apps/{app.name}/index.html</p>
                  )}
                </div>
              </div>
              <button
                type="button"
                data-testid={`space-app-open-${app.name}`}
                onClick={() => setAppToRun(app)}
                className="mt-auto inline-flex h-8 items-center justify-center gap-1 rounded-md bg-accent px-3 text-xs font-bold text-accent-fg transition-colors hover:opacity-90"
              >
                <svg className="h-3.5 w-3.5" viewBox="0 0 16 16" fill="currentColor" stroke="none">
                  <path d="M5 3.5v9l7-4.5z" />
                </svg>
                {t('apps.open')}
              </button>

              {canManage && <AppShareControls spaceId={spaceId} appName={app.name} />}
            </div>
          ))}
        </div>
      )}

      {appToRun && (
        <AppRunner
          gateway={createSpaceGateway(spaceId)}
          appName={appToRun.name}
          entryPath={appToRun.entryPath}
          onClose={() => setAppToRun(null)}
        />
      )}
    </div>
  );
}

/**
 * Discover workspace apps for a space: list `apps/`, then for each subfolder
 * check for an index.html and read its optional app.json. Folders without an
 * index.html are skipped (not runnable). A missing `apps/` directory simply
 * yields an empty list (the listing call 404s → caught → []).
 */
async function loadWorkspaceApps(spaceId: string): Promise<WorkspaceApp[]> {
  // List the workspace root first (never 404s on a fresh workspace) and only
  // descend into apps/ when that directory actually exists — listing a missing
  // apps/ directly would 404 noisily on every fresh workspace.
  let hasAppsDir = false;
  try {
    const root = await fetchSpaceFiles(spaceId, '');
    hasAppsDir = root.entries.some(e => e.kind === 'directory' && e.name === 'apps');
  } catch {
    return [];
  }
  if (!hasAppsDir) return [];

  let appDirs: string[];
  try {
    const appsRoot = await fetchSpaceFiles(spaceId, 'apps');
    appDirs = appsRoot.entries.filter(e => e.kind === 'directory').map(e => e.name);
  } catch {
    return [];
  }
  if (appDirs.length === 0) return [];

  // Fetch each app folder's listing in parallel to find index.html + app.json.
  const perApp = await Promise.all(
    appDirs.map(async name => {
      try {
        const dir = await fetchSpaceFiles(spaceId, `apps/${name}`);
        const names = new Set(dir.entries.filter(e => e.kind !== 'directory').map(e => e.name));
        const hasIndex = names.has('index.html');
        let manifest: string | null = null;
        if (hasIndex && names.has('app.json')) {
          try {
            manifest = await fetchSpaceFileContent(spaceId, `apps/${name}/app.json`);
          } catch {
            manifest = null; // tolerant: a bad/unreadable manifest falls back to defaults
          }
        }
        return { name, hasIndex, manifest };
      } catch {
        return { name, hasIndex: false, manifest: null };
      }
    }),
  );

  const indexMap = new Map(perApp.map(p => [p.name, p]));
  return deriveAppList(
    appDirs,
    name => indexMap.get(name)?.hasIndex ?? false,
    name => indexMap.get(name)?.manifest ?? null,
  );
}

/**
 * AppShareControls — 1 アプリの公開共有リンク管理（canManage のみ表示）。
 *
 * 現在のリンク状態を取得し、未発行なら「リンクを作成」、発行済みなら公開 URL の表示・
 * コピー・失効を提供する。発行/失効は react-query mutation。公開リンクは read-only・
 * ログイン不要・apps/{app}/＋output/ に封じ込められる旨を注意書きで明示する。
 */
function AppShareControls({ spaceId, appName }: { spaceId: string; appName: string }) {
  const { t } = useTranslation('spaces');
  const qc = useQueryClient();
  const [copied, setCopied] = useState(false);
  const queryKey = ['app-share-link', spaceId, appName];

  const linkQuery = useQuery({
    queryKey,
    queryFn: () => getAppShareLink(spaceId, appName),
    staleTime: 10_000,
  });

  const createMut = useMutation({
    mutationFn: () => createAppShareLink(spaceId, appName),
    onSuccess: (data) => {
      qc.setQueryData(queryKey, { token: data.token, shareUrl: data.shareUrl, revokedAt: null });
    },
  });

  const revokeMut = useMutation({
    mutationFn: () => revokeAppShareLink(spaceId, appName),
    onSuccess: () => {
      qc.setQueryData(queryKey, { token: null, revokedAt: new Date().toISOString() });
      setCopied(false);
    },
  });

  const link = linkQuery.data;
  const shareUrl = link?.token && link.shareUrl ? link.shareUrl : null;
  const displayUrl = shareUrl ? buildAppShareDisplayUrl(window.location.origin, shareUrl) : null;

  const copy = async () => {
    if (!displayUrl) return;
    try {
      await navigator.clipboard.writeText(displayUrl);
      setCopied(true);
      window.setTimeout(() => setCopied(false), 1800);
    } catch {
      // clipboard 不可（権限なし等）。表示済み URL から手動コピーできるので無視。
    }
  };

  const onRevoke = () => {
    if (window.confirm(t('appShare.revokeConfirm', { appName }))) {
      revokeMut.mutate();
    }
  };

  const busy = createMut.isPending || revokeMut.isPending;

  return (
    <div
      data-testid={`space-app-share-${appName}`}
      className="mt-1 border-t border-hairline pt-2"
    >
      {linkQuery.isLoading ? (
        <p className="text-2xs text-slate-400">{t('appShare.checking')}</p>
      ) : displayUrl ? (
        <div className="flex flex-col gap-1.5">
          <div className="flex items-center gap-1.5">
            <span
              className="inline-flex items-center gap-1 rounded bg-emerald-500/10 px-1.5 py-0.5 text-2xs font-semibold text-emerald-600 dark:text-emerald-400"
              data-testid={`space-app-share-badge-${appName}`}
            >
              <svg className="h-3 w-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
                <path d="M6.5 9.5l-2 2a2.5 2.5 0 01-3.5-3.5l2-2M9.5 6.5l2-2a2.5 2.5 0 013.5 3.5l-2 2M5.5 10.5l5-5" />
              </svg>
              {t('appShare.issued')}
            </span>
          </div>
          <input
            type="text"
            readOnly
            value={displayUrl}
            data-testid={`space-app-share-url-${appName}`}
            onFocus={(e) => e.currentTarget.select()}
            className="w-full truncate rounded border border-hairline bg-canvas px-2 py-1 font-mono text-2xs text-slate-600"
          />
          <div className="flex items-center gap-1.5">
            <button
              type="button"
              data-testid={`space-app-share-copy-${appName}`}
              onClick={() => void copy()}
              className="inline-flex h-7 items-center rounded-md border border-hairline bg-canvas px-2 text-2xs font-semibold text-slate-600 transition-colors hover:bg-surface"
            >
              {copied ? t('appShare.copied') : t('appShare.copyUrl')}
            </button>
            <button
              type="button"
              data-testid={`space-app-share-revoke-${appName}`}
              onClick={onRevoke}
              disabled={busy}
              className="inline-flex h-7 items-center rounded-md border border-hairline bg-canvas px-2 text-2xs font-semibold text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50 dark:hover:bg-red-950/30"
            >
              {t('appShare.revoke')}
            </button>
          </div>
          <p className="text-2xs leading-relaxed text-amber-600">
            <Trans
              t={t}
              i18nKey="appShare.warning"
              components={{ code: <code className="rounded bg-canvas px-1" />, strong: <strong /> }}
            />
          </p>
        </div>
      ) : (
        <div className="flex flex-col gap-1">
          <button
            type="button"
            data-testid={`space-app-share-create-${appName}`}
            onClick={() => createMut.mutate()}
            disabled={busy}
            className="inline-flex h-7 items-center gap-1 self-start rounded-md border border-hairline bg-canvas px-2 text-2xs font-semibold text-slate-600 transition-colors hover:bg-surface disabled:opacity-50"
          >
            <svg className="h-3 w-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
              <path d="M6.5 9.5l-2 2a2.5 2.5 0 01-3.5-3.5l2-2M9.5 6.5l2-2a2.5 2.5 0 013.5 3.5l-2 2M5.5 10.5l5-5" />
            </svg>
            {createMut.isPending ? t('appShare.creating') : t('appShare.create')}
          </button>
          {(createMut.isError || revokeMut.isError) && (
            <p className="text-2xs text-red-600">{t('appShare.opError')}</p>
          )}
        </div>
      )}
    </div>
  );
}
