<script setup lang="ts">
import type { FormInstance } from 'element-plus';

import type {
  DocumentFileNode,
  DocumentValidationIssue,
} from '@easyflow/common-ui';

import type {
  EditorCursorPosition,
  EditorLanguage,
} from '@easyflow-core/editor-ui';

import type {
  SkillFileContent,
  SkillFileNode,
  SkillValidationIssue,
} from './types';

import {
  computed,
  defineAsyncComponent,
  nextTick,
  onBeforeUnmount,
  onMounted,
  reactive,
  ref,
  watch,
} from 'vue';

import {
  DocumentEditorWorkbench,
  EasyFlowFormModal,
  EditorStatusBar,
  FileTreePanel,
  ValidationPanel,
} from '@easyflow/common-ui';
import { downloadFileFromBlob } from '@easyflow/utils';

import {
  CodeEditor,
  CodeViewer,
  EMPTY_IMAGE_DATA_URL,
  MarkdownSourceEditor,
  MAX_HIGHLIGHTED_TEXT_CHARACTERS,
  MAX_LIVE_MARKDOWN_CHARACTERS,
  resolveSafeExternalMarkdownImageUrl,
} from '@easyflow-core/editor-ui';

import {
  Connection,
  CopyDocument,
  Download,
  Fold,
  FullScreen,
  Search,
} from '@element-plus/icons-vue';
import { onKeyStroke } from '@vueuse/core';
import {
  ElAlert,
  ElButton,
  ElEmpty,
  ElForm,
  ElFormItem,
  ElInput,
  ElMessage,
  ElMessageBox,
  ElOption,
  ElSegmented,
  ElSelect,
  ElTag,
} from 'element-plus';

import {
  createSkillFile,
  deleteSkillFile,
  downloadSkillFile,
  getSkillFileContent,
  getSkillFileTree,
  previewSkillFile,
  renameSkillFile,
  saveSkillFile,
  uploadSkillFile,
  validateSkillForPublish,
} from './api';
import { isSkillFileConflictError } from './skill-api-error';
import { buildSkillTextDownload } from './skill-file-download';
import { isSkillPathText } from './skill-file-representation';
import { resolveSkillRelativePath, splitSkillMarkdown } from './skill-markdown';
import { useSkillFileBuffers } from './use-skill-file-buffers';

const props = withDefaults(
  defineProps<{
    activePanel?: 'capability' | 'resources';
    capabilityAvailable?: boolean;
    capabilityCount?: number;
    initialContent?: string;
    readonly?: boolean;
    skillId: number | string;
  }>(),
  {
    activePanel: 'resources',
    capabilityAvailable: false,
    capabilityCount: 0,
    initialContent: '',
    readonly: false,
  },
);

const emit = defineEmits<{
  dirty: [dirty: boolean];
  issues: [issues: SkillValidationIssue[]];
  locateCapability: [];
  newContent: [content: string];
  requestSave: [];
  saved: [];
  'update:activePanel': [panel: 'capability' | 'resources'];
}>();

const MarkdownLiveEditor = defineAsyncComponent(
  () => import('@easyflow-core/editor-ui/markdown-live-editor'),
);
const MAX_TEXT_FILE_BYTES = 2 * 1024 * 1024;
const STANDARD_DIRECTORIES = ['references', 'scripts', 'assets'] as const;

const isNew = computed(() => String(props.skillId) === 'new');
const tree = ref<SkillFileNode[]>([]);
const selectedPath = ref('SKILL.md');
const loading = ref(false);
const validating = ref(false);
const savingAll = ref(false);
const loadError = ref('');
const fileMutation = ref<
  | 'create'
  | 'delete'
  | 'inline-upload'
  | 'reload'
  | 'rename'
  | 'upload'
  | undefined
>();
const issues = ref<SkillValidationIssue[]>([]);
const inspector = ref<'validation' | undefined>();
const markdownMode = ref<'live' | 'source'>('live');
const lineWrapping = ref(true);
const fullscreen = ref(false);
const cursor = ref<EditorCursorPosition>({ column: 1, line: 1 });
const codeEditorRef = ref<InstanceType<typeof CodeEditor>>();
const sourceEditorRef = ref<InstanceType<typeof MarkdownSourceEditor>>();
const uploadInputRef = ref<HTMLInputElement>();
const createFileFormRef = ref<FormInstance>();
const createFileNameRef = ref<InstanceType<typeof ElInput>>();
const createFileDialogOpen = ref(false);
const createFileError = ref('');
const uploadDirectory = ref('assets');
const createFileForm = reactive({ directory: 'references', fileName: '' });
const createFileRules = {
  fileName: [
    { message: '请输入文件名', required: true, trigger: 'blur' },
    {
      trigger: ['blur', 'change'],
      validator: (
        _rule: unknown,
        value: string,
        callback: (error?: Error) => void,
      ) => {
        const fileName = value.trim();
        if (!fileName || fileName.includes('/') || fileName.includes('\\')) {
          callback(new Error('请输入文件名，无需填写目录'));
          return;
        }
        if (fileName === '.' || fileName === '..' || fileName.startsWith('.')) {
          callback(new Error('文件名不能是隐藏文件、. 或 ..'));
          return;
        }
        callback();
      },
    },
  ],
};
const assetPreviewUrl = ref('');
const assetPreviewLoading = ref(false);
const assetPreviewError = ref(false);
const markdownPreviewUrls = new Map<string, string>();
const markdownPreviewPromises = new Map<string, Promise<string>>();
const contentMetrics = ref({ bytes: 0, lines: 1 });
let assetPreviewRequest = 0;
let assetRenderTimer: number | undefined;
let fileContentRequest = 0;
let initRequest = 0;
let loadingRequest = 0;
let markdownPreviewGeneration = 0;
let metricsTimer: number | undefined;
let settledPath = 'SKILL.md';
let treeRequest = 0;
let disposed = false;

const {
  buffers,
  clearBuffers,
  dirtyPaths,
  ensureBuffer,
  markConflict,
  markError,
  markSaved,
  markSaving,
  removeBuffer,
  renameBuffer,
  updateContent,
} = useSkillFileBuffers();

const currentBuffer = computed(() => buffers.get(selectedPath.value));
const currentFile = computed(() => currentBuffer.value?.file);
const currentNode = computed(() =>
  findFileNode(tree.value, selectedPath.value),
);
const currentContent = computed({
  get: () => currentBuffer.value?.content || '',
  set: (value: string) => updateContent(selectedPath.value, value),
});
const isBinaryResource = computed(() => currentFile.value?.isText === false);
const isMarkdown = computed(
  () =>
    !isBinaryResource.value &&
    (currentFile.value?.type === 'SKILL' ||
      normalizeMediaType(currentFile.value?.mediaType) === 'text/markdown' ||
      /\.(?:md|markdown)$/i.test(currentFile.value?.path || '')),
);
const isScript = computed(
  () =>
    !isBinaryResource.value &&
    (currentFile.value?.type === 'SCRIPT' ||
      /\.(?:js|mjs|py|sh)$/.test(currentFile.value?.path.toLowerCase() || '')),
);
const isImageResource = computed(
  () =>
    isBinaryResource.value &&
    (isSafeImageMediaType(currentFile.value?.mediaType || '') ||
      /\.(?:avif|gif|jpe?g|png|webp)$/i.test(currentFile.value?.path || '')),
);
const isPdfResource = computed(
  () =>
    isBinaryResource.value &&
    (normalizeMediaType(currentFile.value?.mediaType) === 'application/pdf' ||
      /\.pdf$/i.test(currentFile.value?.path || '')),
);
const isLargeForLive = computed(
  () => currentContent.value.length > MAX_LIVE_MARKDOWN_CHARACTERS,
);
const isLargeForHighlight = computed(
  () => currentContent.value.length > MAX_HIGHLIGHTED_TEXT_CHARACTERS,
);
const markdownModeOptions = computed(() => [
  {
    disabled: isLargeForLive.value,
    label: '实时编辑',
    value: 'live',
  },
  { label: '源码', value: 'source' },
]);
const markdownParts = computed(() => splitSkillMarkdown(currentContent.value));
const liveMarkdownBody = computed({
  get: () => currentContent.value,
  set: (value: string) => {
    currentContent.value = value;
  },
});
const directoryOptions = computed(() => flattenDirectories(tree.value));
const wordCount = computed(
  () =>
    (isMarkdown.value
      ? markdownParts.value.body
      : currentContent.value
    ).replaceAll(/\s+/g, '').length,
);
const editorLanguage = computed<EditorLanguage>(() => {
  const language = String(currentFile.value?.language || '').toUpperCase();
  if (language === 'PYTHON' || currentFile.value?.path.endsWith('.py'))
    return 'python';
  if (
    language === 'JAVASCRIPT' ||
    /\.(?:js|mjs)$/.test(currentFile.value?.path || '')
  )
    return 'javascript';
  if (language === 'SHELL' || currentFile.value?.path.endsWith('.sh'))
    return 'shell';
  if (isMarkdown.value) return 'markdown';
  if (currentFile.value?.mediaType === 'application/json') return 'json';
  return 'text';
});
const lineCount = computed(() => contentMetrics.value.lines);
const sizeLabel = computed(() => formatBytes(contentMetrics.value.bytes));
const errorCounts = computed(() => groupIssueCounts('ERROR'));
const warningCounts = computed(() => groupIssueCounts('WARNING'));

watch(
  dirtyPaths,
  (value) => {
    emit('dirty', value.size > 0);
    if (value.has('SKILL.md')) {
      emit('newContent', buffers.get('SKILL.md')?.content || '');
    }
  },
  { deep: true },
);
watch(
  [selectedPath, currentContent],
  ([path, content], previous) => {
    window.clearTimeout(metricsTimer);
    const previousPath = previous?.[0];
    if (path !== previousPath || content.length < 100_000) {
      updateContentMetrics(content);
      return;
    }
    metricsTimer = window.setTimeout(() => updateContentMetrics(content), 180);
  },
  { immediate: true },
);
watch([selectedPath, isLargeForLive, isMarkdown], ([, large, markdown]) => {
  cursor.value = { column: 1, line: 1 };
  if (markdown && large && markdownMode.value === 'live') {
    markdownMode.value = 'source';
    ElMessage.info('文件较大，已切换到源码模式以保持编辑流畅');
  }
});

watch(() => props.skillId, init);
watch(
  () => props.initialContent,
  (content) => {
    if (!isNew.value) return;
    const buffer = buffers.get('SKILL.md');
    if (buffer && content !== buffer.content) {
      updateContent('SKILL.md', content || '');
    }
  },
);
watch([selectedPath, currentFile], loadAssetPreview);
onMounted(init);
onKeyStroke('Escape', () => {
  if (fullscreen.value) fullscreen.value = false;
});
onBeforeUnmount(() => {
  disposed = true;
  fileContentRequest++;
  initRequest++;
  loadingRequest++;
  treeRequest++;
  window.clearTimeout(metricsTimer);
  releasePreviewUrls();
});

async function init() {
  const request = ++initRequest;
  const requestedSkillId = String(props.skillId);
  fileContentRequest++;
  treeRequest++;
  const currentLoadingRequest = ++loadingRequest;
  loading.value = true;
  loadError.value = '';
  assetPreviewRequest++;
  clearMarkdownPreviewUrls();
  clearBuffers();
  issues.value = [];
  selectedPath.value = 'SKILL.md';
  settledPath = 'SKILL.md';
  try {
    if (isNew.value) {
      const content =
        props.initialContent ||
        '---\nname: new-skill\ndescription: New skill\n---\n\n# Instructions\n';
      const file: SkillFileContent = {
        content,
        path: 'SKILL.md',
        type: 'SKILL',
      };
      if (!isCurrentRequest(requestedSkillId, request)) return;
      ensureBuffer(file);
      tree.value = ensureStandardTree([
        { key: 'SKILL.md', name: 'SKILL.md', path: 'SKILL.md', type: 'SKILL' },
      ]);
      return;
    }
    if (!(await loadTree(request))) return;
    await selectFile('SKILL.md', request);
  } catch {
    if (isCurrentRequest(requestedSkillId, request)) {
      loadError.value = '文件加载失败，请重试';
    }
  } finally {
    if (
      isCurrentRequest(requestedSkillId, request) &&
      currentLoadingRequest === loadingRequest
    ) {
      loading.value = false;
    }
  }
}

async function loadTree(expectedInitRequest?: number) {
  const request = ++treeRequest;
  const requestedSkillId = String(props.skillId);
  const res = await getSkillFileTree(requestedSkillId);
  if (res.errorCode !== 0) throw new Error(res.message || '文件树加载失败');
  if (
    disposed ||
    request !== treeRequest ||
    String(props.skillId) !== requestedSkillId ||
    (expectedInitRequest !== undefined && expectedInitRequest !== initRequest)
  ) {
    return false;
  }
  tree.value = ensureStandardTree(res.data || []);
  return true;
}

async function selectFile(
  pathOrNode: string | { path: string; type?: string },
  expectedInitRequest?: number,
) {
  const path = typeof pathOrNode === 'string' ? pathOrNode : pathOrNode.path;
  if (typeof pathOrNode !== 'string' && pathOrNode.type === 'DIRECTORY') {
    fileContentRequest++;
    loadingRequest++;
    loading.value = false;
    loadError.value = '';
    selectedPath.value = path;
    settledPath = path;
    return true;
  }
  const request = ++fileContentRequest;
  const requestedSkillId = String(props.skillId);
  const previousPath = settledPath;
  selectedPath.value = path;
  if (buffers.has(path) || isNew.value) {
    settledPath = path;
    loadingRequest++;
    loading.value = false;
    return true;
  }
  const currentLoadingRequest = ++loadingRequest;
  loading.value = true;
  loadError.value = '';
  try {
    const res = await getSkillFileContent(requestedSkillId, path);
    if (res.errorCode !== 0)
      throw new Error(res.message || `文件加载失败：${path}`);
    if (!isCurrentFileRequest(request, requestedSkillId, expectedInitRequest)) {
      return false;
    }
    ensureBuffer(res.data);
    settledPath = path;
    return true;
  } catch {
    if (isCurrentFileRequest(request, requestedSkillId, expectedInitRequest)) {
      selectedPath.value = previousPath;
      loadError.value = `无法加载 ${path}，请重试`;
    }
    return false;
  } finally {
    if (
      isCurrentFileRequest(request, requestedSkillId, expectedInitRequest) &&
      currentLoadingRequest === loadingRequest
    ) {
      loading.value = false;
    }
  }
}

function isCurrentRequest(skillId: string, request: number) {
  return (
    !disposed && request === initRequest && String(props.skillId) === skillId
  );
}

function isCurrentFileRequest(
  request: number,
  skillId: string,
  expectedInitRequest?: number,
) {
  return (
    !disposed &&
    request === fileContentRequest &&
    String(props.skillId) === skillId &&
    (expectedInitRequest === undefined || expectedInitRequest === initRequest)
  );
}

async function selectFileFromTree(
  node: { path: string; type?: string },
  closeTree: () => void,
) {
  emit('update:activePanel', 'resources');
  await selectFile(node);
  closeTree();
}

function selectCapabilityPanel(closeTree: () => void) {
  if (!props.capabilityAvailable) return;
  inspector.value = undefined;
  fullscreen.value = false;
  emit('update:activePanel', 'capability');
  closeTree();
}

async function saveBuffer(path: string) {
  const buffer = buffers.get(path);
  if (!buffer || buffer.content === buffer.original || isNew.value) return true;
  if (buffer.saveState === 'saving') return false;
  if (buffer.conflict) {
    markError(path);
    ElMessage.error('远端文件已变化，请重新加载并合并后再保存');
    return false;
  }
  if (new TextEncoder().encode(buffer.content).length > MAX_TEXT_FILE_BYTES) {
    markError(path);
    ElMessage.error('单个文本文件不能超过 2 MB');
    return false;
  }
  const sentContent = markSaving(path);
  try {
    const res = await saveSkillFile(
      props.skillId,
      path,
      sentContent || '',
      buffer.file.contentHash,
    );
    if (res.errorCode !== 0) {
      markError(path);
      return false;
    }
    markSaved(path, res.data, sentContent);
    clearMarkdownPreviewUrls();
    return true;
  } catch (error) {
    if (isSkillFileConflictError(error)) {
      markConflict(path);
      ElMessage.error(
        '文件已在其他位置更新。本地内容已保留，请重新加载后合并。',
      );
    } else {
      markError(path);
    }
    return false;
  }
}

async function reloadCurrent() {
  const buffer = currentBuffer.value;
  if (!buffer || isNew.value || fileMutation.value || savingAll.value) return;
  fileMutation.value = 'reload';
  try {
    if (buffer.content !== buffer.original) {
      await ElMessageBox.confirm(
        '重新加载会丢弃当前文件的本地修改。可先复制内容，再继续。',
        '重新加载文件',
        {
          cancelButtonText: '取消',
          confirmButtonText: '重新加载',
          type: 'warning',
        },
      );
    }
    const res = await getSkillFileContent(props.skillId, buffer.file.path);
    if (res.errorCode === 0) {
      ensureBuffer(res.data, 'replace');
      ElMessage.success('已加载服务端最新内容');
    }
  } catch {
    // Confirmation cancellation and request failures need no extra message.
  } finally {
    fileMutation.value = undefined;
  }
}

async function saveAll() {
  if (savingAll.value || fileMutation.value) return false;
  savingAll.value = true;
  try {
    if (isNew.value) {
      emit('newContent', buffers.get('SKILL.md')?.content || '');
      return true;
    }
    for (const path of dirtyPaths.value) {
      if (!(await saveBuffer(path))) return false;
    }
    emit('saved');
    await loadTree();
    return true;
  } finally {
    savingAll.value = false;
  }
}

async function validateAll() {
  if (validating.value || savingAll.value || fileMutation.value) return false;
  if (isNew.value) {
    issues.value = [];
    emit('issues', []);
    return true;
  }
  validating.value = true;
  try {
    const res = await validateSkillForPublish(props.skillId);
    if (res.errorCode !== 0) return false;
    issues.value = res.data.issues || [];
    emit('issues', issues.value);
    inspector.value = 'validation';
    if (res.data.valid) ElMessage.success('发布级校验通过');
    else ElMessage.warning('发现需要处理的校验问题');
    return res.data.valid;
  } finally {
    validating.value = false;
  }
}

async function locateIssue(issue: DocumentValidationIssue) {
  if (!issue.path) return;
  if (issue.path.startsWith('capabilities')) {
    inspector.value = undefined;
    emit('locateCapability');
    return;
  }
  if (!flattenFilePaths(tree.value).includes(issue.path)) {
    ElMessage.warning(`无法定位校验项：${issue.path}`);
    return;
  }
  await selectFile(issue.path);
  inspector.value = undefined;
  await nextTick();
  if (issue.line) {
    if (isMarkdown.value) markdownMode.value = 'source';
    await nextTick();
    sourceEditorRef.value?.scrollToLine?.(issue.line, issue.column || 1);
    codeEditorRef.value?.scrollToLine(issue.line, issue.column || 1);
  }
}

function openCreateFile(node?: DocumentFileNode) {
  if (props.readonly || fileMutation.value || savingAll.value) return;
  if (isNew.value) {
    ElMessage.info('请先保存 Skill，再添加资源文件');
    return;
  }
  const selectedDirectory =
    node?.type === 'DIRECTORY'
      ? node.path
      : currentNode.value?.type === 'DIRECTORY'
        ? currentNode.value.path
        : selectedPath.value.includes('/')
          ? selectedPath.value.slice(0, selectedPath.value.lastIndexOf('/'))
          : 'references';
  createFileForm.directory = directoryOptions.value.some(
    (option) => option.path === selectedDirectory,
  )
    ? selectedDirectory
    : 'references';
  createFileForm.fileName = '';
  createFileError.value = '';
  createFileDialogOpen.value = true;
  void nextTick(() => createFileNameRef.value?.focus());
}

async function submitCreateFile() {
  if (fileMutation.value || savingAll.value) return;
  const valid = await createFileFormRef.value?.validate().catch(() => false);
  if (!valid) return;
  const path = `${createFileForm.directory}/${createFileForm.fileName.trim()}`;
  const pathError = resourcePathError(path);
  if (pathError) {
    createFileError.value = pathError;
    return;
  }
  if (findFileNode(tree.value, path)) {
    createFileError.value = '同名文件已存在';
    return;
  }
  fileMutation.value = 'create';
  createFileError.value = '';
  try {
    const res = await createSkillFile(props.skillId, path);
    if (res.errorCode === 0) {
      await loadTree();
      ensureBuffer(res.data);
      selectedPath.value = res.data.path;
      settledPath = res.data.path;
      createFileDialogOpen.value = false;
      ElMessage.success('文件已创建');
    } else {
      createFileError.value = res.message || '文件创建失败，请重试';
    }
  } catch (error) {
    createFileError.value =
      error instanceof Error ? error.message : '文件创建失败，请重试';
  } finally {
    fileMutation.value = undefined;
  }
}

function chooseUpload(node?: DocumentFileNode) {
  if (props.readonly || fileMutation.value || savingAll.value) return;
  if (isNew.value) {
    ElMessage.info('请先保存 Skill，再上传资源');
    return;
  }
  uploadDirectory.value =
    node?.type === 'DIRECTORY'
      ? node.path
      : currentNode.value?.type === 'DIRECTORY'
        ? currentNode.value.path
        : 'assets';
  uploadInputRef.value?.click();
}

async function handleUpload(event: Event) {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  input.value = '';
  if (!file || fileMutation.value || savingAll.value) return;
  if (file.size > 50 * 1024 * 1024) {
    ElMessage.warning('单个资源文件不能超过 50 MB');
    return;
  }
  fileMutation.value = 'upload';
  const safeName = file.name.replaceAll(/[^\w.-]/g, '-');
  const targetPath = `${uploadDirectory.value}/${safeName}`;
  try {
    const existingNode = findFileNode(tree.value, targetPath);
    if (existingNode?.type === 'DIRECTORY') {
      ElMessage.warning('目标位置是目录，请调整文件名后重试');
      return;
    }
    const existingBuffer = buffers.get(targetPath);
    const expectedContentHash =
      existingBuffer?.file.contentHash || existingNode?.contentHash;
    if (existingNode) {
      if (!expectedContentHash) {
        ElMessage.warning('资源版本信息缺失，请刷新后重试');
        return;
      }
      const replaceMessage =
        existingBuffer?.content === existingBuffer?.original
          ? `${targetPath} 已存在，确认替换该资源？`
          : `${targetPath} 有未保存修改，替换后本地修改会丢失。`;
      await ElMessageBox.confirm(replaceMessage, '替换已有资源', {
        cancelButtonText: '取消',
        confirmButtonText: '替换',
        type: 'warning',
      });
    }
    const res = await uploadSkillFile(
      props.skillId,
      targetPath,
      file,
      expectedContentHash,
    );
    if (res.errorCode === 0) {
      ensureBuffer(res.data, 'replace');
      selectedPath.value = targetPath;
      settledPath = targetPath;
      await loadTree();
      clearMarkdownPreviewUrls();
      await loadAssetPreview();
      ElMessage.success(existingNode ? '资源已替换' : '资源已上传');
    }
  } catch (error) {
    if (isSkillFileConflictError(error)) {
      if (buffers.has(targetPath)) markConflict(targetPath);
      ElMessage.error('资源已在其他位置更新，请刷新文件后再替换');
    }
  } finally {
    fileMutation.value = undefined;
  }
}

async function uploadInlineImage(file: File) {
  if (props.readonly) throw new Error('当前没有文件管理权限');
  if (
    !isSafeImageMediaType(file.type) ||
    !/\.(?:avif|gif|jpe?g|png|webp)$/i.test(file.name)
  ) {
    throw new Error('仅支持 PNG、JPEG、GIF、WebP 或 AVIF 图片');
  }
  if (file.size > 50 * 1024 * 1024)
    throw new Error('单个资源文件不能超过 50 MB');
  if (fileMutation.value || savingAll.value)
    throw new Error('另一个文件操作正在进行，请稍后重试');
  fileMutation.value = 'inline-upload';
  const safeName = file.name.replaceAll(/[^\w.-]/g, '-');
  const path = `assets/${Date.now()}-${safeName}`;
  try {
    const res = await uploadSkillFile(props.skillId, path, file);
    if (res.errorCode !== 0) throw new Error(res.message || '图片上传失败');
    await loadTree();
    return path;
  } finally {
    fileMutation.value = undefined;
  }
}

async function renameCurrent() {
  if (
    !currentFile.value ||
    currentFile.value.path === 'SKILL.md' ||
    isNew.value ||
    fileMutation.value ||
    savingAll.value
  )
    return;
  const currentPath = currentFile.value.path;
  const currentIsText = Boolean(currentFile.value.isText);
  const hasUnsavedContent =
    currentBuffer.value?.content !== currentBuffer.value?.original;
  fileMutation.value = 'rename';
  try {
    const { value } = await ElMessageBox.prompt(
      '输入新的 Skill 相对路径。',
      '重命名文件',
      {
        confirmButtonText: '重命名',
        inputValidator: (value: string) => resourcePathError(value) || true,
        inputValue: currentPath,
      },
    );
    const nextPath = value.trim();
    if (!nextPath || nextPath === currentPath) return;
    if (buffers.has(nextPath) || findFileNode(tree.value, nextPath)) {
      ElMessage.warning(`目标路径已存在：${nextPath}`);
      return;
    }
    if (hasUnsavedContent && isSkillPathText(nextPath) !== currentIsText) {
      ElMessage.warning('跨文本与二进制类型重命名前，请先保存或放弃当前修改');
      return;
    }
    const expectedContentHash = currentBuffer.value?.file.contentHash;
    if (!expectedContentHash) {
      ElMessage.warning('文件版本信息缺失，请重新加载后重试');
      return;
    }
    if (hasUnsavedContent) {
      await ElMessageBox.confirm(
        '当前文件有未保存修改。重命名后本地修改会继续保留。',
        '重命名文件',
        {
          cancelButtonText: '取消',
          confirmButtonText: '继续重命名',
          type: 'warning',
        },
      );
    }
    const res = await renameSkillFile(
      props.skillId,
      currentPath,
      nextPath,
      expectedContentHash,
    );
    if (res.errorCode !== 0) return;
    renameBuffer(currentPath, nextPath);
    ensureBuffer(res.data);
    selectedPath.value = nextPath;
    settledPath = nextPath;
    await loadTree();
    ElMessage.success('文件已重命名');
  } catch (error) {
    if (isSkillFileConflictError(error)) {
      markConflict(currentPath);
      ElMessage.error('文件已在其他位置更新，请重新加载后再重命名');
    } else if (
      error instanceof Error &&
      error.message.startsWith('目标路径已存在')
    ) {
      ElMessage.warning(error.message);
    }
  } finally {
    fileMutation.value = undefined;
  }
}

async function removeCurrent() {
  if (
    !currentFile.value ||
    currentFile.value.path === 'SKILL.md' ||
    isNew.value ||
    fileMutation.value ||
    savingAll.value
  )
    return;
  const currentPath = currentFile.value.path;
  const expectedContentHash = currentBuffer.value?.file.contentHash;
  if (!expectedContentHash) {
    ElMessage.warning('文件版本信息缺失，请重新加载后重试');
    return;
  }
  const hasUnsavedContent =
    currentBuffer.value?.content !== currentBuffer.value?.original;
  fileMutation.value = 'delete';
  try {
    await ElMessageBox.confirm(
      hasUnsavedContent
        ? `${currentPath} 有未保存修改，删除后无法恢复。`
        : `确认删除 ${currentPath}？`,
      '删除文件',
      {
        cancelButtonText: '取消',
        confirmButtonText: '删除',
        type: 'warning',
      },
    );
    const res = await deleteSkillFile(
      props.skillId,
      currentPath,
      expectedContentHash,
    );
    if (res.errorCode !== 0) return;
    removeBuffer(currentPath);
    selectedPath.value = 'SKILL.md';
    await loadTree();
    await selectFile('SKILL.md');
    ElMessage.success('文件已删除');
  } catch (error) {
    if (isSkillFileConflictError(error)) {
      markConflict(currentPath);
      ElMessage.error('文件已在其他位置更新，请重新加载后再删除');
    }
  } finally {
    fileMutation.value = undefined;
  }
}

async function renameNode(node: DocumentFileNode) {
  if (await selectFile(node)) await renameCurrent();
}

async function removeNode(node: DocumentFileNode) {
  if (await selectFile(node)) await removeCurrent();
}

async function downloadNode(node: DocumentFileNode) {
  if (await selectFile(node)) await downloadCurrent();
}

async function downloadCurrent() {
  if (!currentFile.value || isNew.value) return;
  try {
    const blob =
      buildSkillTextDownload(currentFile.value, currentContent.value) ||
      (await downloadSkillFile(props.skillId, currentFile.value.path));
    downloadFileFromBlob({
      fileName: currentFile.value.path.split('/').pop() || 'resource',
      source: blob,
    });
  } catch {
    // The shared request layer presents the actionable error message.
  }
}

async function copyCurrent() {
  try {
    await navigator.clipboard.writeText(currentContent.value);
    ElMessage.success('已复制');
  } catch {
    ElMessage.error('复制失败，请检查浏览器权限');
  }
}

async function loadAssetPreview() {
  const request = ++assetPreviewRequest;
  window.clearTimeout(assetRenderTimer);
  if (assetPreviewUrl.value) URL.revokeObjectURL(assetPreviewUrl.value);
  assetPreviewUrl.value = '';
  assetPreviewLoading.value = false;
  assetPreviewError.value = false;
  if (
    !isBinaryResource.value ||
    isNew.value ||
    (!isImageResource.value && !isPdfResource.value)
  )
    return;
  assetPreviewLoading.value = true;
  try {
    const blob = await previewSkillFile(props.skillId, selectedPath.value);
    const responseMediaType = normalizeMediaType(blob.type);
    const safeResponse = isImageResource.value
      ? isSafeImageMediaType(responseMediaType)
      : responseMediaType === 'application/pdf';
    if (!safeResponse) throw new Error('预览内容类型与资源类型不匹配');
    const objectUrl = URL.createObjectURL(blob);
    if (disposed || request !== assetPreviewRequest) {
      URL.revokeObjectURL(objectUrl);
      return;
    }
    assetPreviewUrl.value = objectUrl;
    assetRenderTimer = window.setTimeout(() => {
      if (request === assetPreviewRequest && assetPreviewLoading.value) {
        handleAssetRenderError();
      }
    }, 8000);
  } catch {
    if (!disposed && request === assetPreviewRequest) {
      markAssetPreviewError();
    }
  } finally {
    if (
      !disposed &&
      request === assetPreviewRequest &&
      (!assetPreviewUrl.value || assetPreviewError.value)
    ) {
      assetPreviewLoading.value = false;
    }
  }
}

function handleAssetRendered() {
  window.clearTimeout(assetRenderTimer);
  assetRenderTimer = undefined;
  assetPreviewLoading.value = false;
}

function handleAssetRenderError() {
  window.clearTimeout(assetRenderTimer);
  assetRenderTimer = undefined;
  if (assetPreviewUrl.value) {
    URL.revokeObjectURL(assetPreviewUrl.value);
    assetPreviewUrl.value = '';
  }
  markAssetPreviewError();
  assetPreviewLoading.value = false;
}

function markAssetPreviewError() {
  if (assetPreviewError.value) return;
  assetPreviewError.value = true;
  ElMessage.warning('资源预览加载失败，可下载后查看');
}

async function resolveMarkdownImage(url: string) {
  if (/^https:/i.test(url)) return resolveSafeExternalMarkdownImageUrl(url);
  if (/^[a-z][a-z\d+.-]*:|^\/\//i.test(url)) {
    return EMPTY_IMAGE_DATA_URL;
  }
  if (isNew.value) return EMPTY_IMAGE_DATA_URL;
  const path = resolveSkillRelativePath(selectedPath.value, url);
  const node = path ? findFileNode(tree.value, path) : undefined;
  if (!path || !node || !isSafeImageNode(node)) return EMPTY_IMAGE_DATA_URL;
  const cached = markdownPreviewUrls.get(path);
  if (cached) return cached;
  const pending = markdownPreviewPromises.get(path);
  if (pending) return pending;
  const generation = markdownPreviewGeneration;
  const promise = previewSkillFile(props.skillId, path)
    .then((blob) => {
      if (!isSafeImageMediaType(blob.type)) {
        return EMPTY_IMAGE_DATA_URL;
      }
      const objectUrl = URL.createObjectURL(blob);
      if (disposed || generation !== markdownPreviewGeneration) {
        URL.revokeObjectURL(objectUrl);
        return EMPTY_IMAGE_DATA_URL;
      }
      markdownPreviewUrls.set(path, objectUrl);
      return objectUrl;
    })
    .finally(() => {
      if (markdownPreviewPromises.get(path) === promise) {
        markdownPreviewPromises.delete(path);
      }
    });
  markdownPreviewPromises.set(path, promise);
  return promise;
}

async function handleRelativeLink(href: string) {
  const path = resolveSkillRelativePath(selectedPath.value, href);
  if (!path) return;
  const knownPaths = new Set(flattenFilePaths(tree.value));
  if (!knownPaths.has(path)) {
    ElMessage.warning(`未找到链接资源：${path}`);
    return;
  }
  await selectFile(path);
}

function flattenFilePaths(nodes: SkillFileNode[]): string[] {
  return nodes.flatMap((node) => [
    node.path,
    ...flattenFilePaths(node.children || []),
  ]);
}

function flattenDirectories(nodes: SkillFileNode[]): SkillFileNode[] {
  return nodes.flatMap((node) =>
    node.type === 'DIRECTORY'
      ? [node, ...flattenDirectories(node.children || [])]
      : [],
  );
}

function ensureStandardTree(nodes: SkillFileNode[]) {
  const skill = nodes.find((node) => node.path === 'SKILL.md') || {
    key: 'SKILL.md',
    name: 'SKILL.md',
    path: 'SKILL.md',
    type: 'SKILL',
  };
  const directories = STANDARD_DIRECTORIES.map(
    (path) =>
      nodes.find((node) => node.path === path && node.type === 'DIRECTORY') || {
        children: [],
        key: path,
        name: path,
        path,
        type: 'DIRECTORY',
      },
  );
  const standardPaths = new Set(['SKILL.md', ...STANDARD_DIRECTORIES]);
  return [
    skill,
    ...directories,
    ...nodes.filter((node) => !standardPaths.has(node.path)),
  ];
}

function findFileNode(
  nodes: SkillFileNode[],
  path: string,
): SkillFileNode | undefined {
  for (const node of nodes) {
    if (node.path === path) return node;
    const child = findFileNode(node.children || [], path);
    if (child) return child;
  }
  return undefined;
}

function isSafeImageNode(node: SkillFileNode) {
  if (node.type === 'DIRECTORY' || node.isText === true) return false;
  if (node.mediaType) return isSafeImageMediaType(node.mediaType);
  return /\.(?:avif|gif|jpe?g|png|webp)$/i.test(node.path);
}

function isSafeImageMediaType(mediaType: string) {
  return /^image\/(?:avif|gif|jpeg|png|webp)$/i.test(
    normalizeMediaType(mediaType),
  );
}

function normalizeMediaType(mediaType?: string) {
  return mediaType?.split(';', 1)[0]?.trim().toLowerCase() || '';
}

function resourcePathError(value: string) {
  const path = value.trim();
  if (
    !path ||
    path === 'SKILL.md' ||
    path.startsWith('/') ||
    path.includes('\\') ||
    path.includes('\0')
  ) {
    return '请输入安全的 Skill 相对路径';
  }
  if (path.length > 512) return '路径不能超过 512 个字符';
  const segments = path.split('/');
  if (segments.length > 16) return '路径最多支持 16 层';
  if (
    segments.some(
      (segment) =>
        !segment ||
        segment === '.' ||
        segment === '..' ||
        segment.startsWith('.'),
    )
  ) {
    return '路径不能包含空目录、隐藏目录、. 或 ..';
  }
  return '';
}

function clearMarkdownPreviewUrls() {
  markdownPreviewGeneration++;
  markdownPreviewUrls.forEach((url) => URL.revokeObjectURL(url));
  markdownPreviewUrls.clear();
  markdownPreviewPromises.clear();
}

function releasePreviewUrls() {
  assetPreviewRequest++;
  window.clearTimeout(assetRenderTimer);
  assetRenderTimer = undefined;
  assetPreviewLoading.value = false;
  assetPreviewError.value = false;
  clearMarkdownPreviewUrls();
  if (assetPreviewUrl.value) {
    URL.revokeObjectURL(assetPreviewUrl.value);
    assetPreviewUrl.value = '';
  }
}

function openCurrentSearch() {
  if (isBinaryResource.value) return;
  if (isMarkdown.value) sourceEditorRef.value?.openSearch();
  else codeEditorRef.value?.openSearch();
}

function updateContentMetrics(content: string) {
  contentMetrics.value = {
    bytes: new TextEncoder().encode(content).length,
    lines: Math.max(content.split(/\r?\n/).length, 1),
  };
}

function groupIssueCounts(severity: string) {
  const result: Record<string, number> = {};
  for (const issue of issues.value) {
    if (issue.severity === severity && issue.path) {
      result[issue.path] = (result[issue.path] || 0) + 1;
    }
  }
  return result;
}

function formatBytes(bytes: number) {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}

function handleEditorError(error: Error) {
  ElMessage.warning(error.message);
}

function handleMarkdownFidelityLoss() {
  markdownMode.value = 'source';
  ElMessage.warning(
    '当前 Markdown 包含实时编辑器无法保真处理的语法，已切换到源码模式',
  );
}

function handleFrontmatterActivate() {
  if (props.readonly) {
    ElMessage.info('当前为只读模式，无法编辑 SKILL.md 元数据');
    return;
  }
  markdownMode.value = 'source';
  void nextTick(() => sourceEditorRef.value?.focus?.());
}

defineExpose({
  getSkillContent: () => buffers.get('SKILL.md')?.content,
  hasDirty: () => dirtyPaths.value.size > 0,
  locateIssue,
  saveAll,
  validateAll,
});
</script>

<template>
  <DocumentEditorWorkbench
    class="skill-resource-workbench"
    :class="{ 'is-fullscreen': fullscreen }"
    :inspector-open="Boolean(inspector)"
    tree-label="Skill 内容"
    @close-inspector="inspector = undefined"
  >
    <template #tree="{ close }">
      <div class="skill-resource-workbench__tree-layout">
        <FileTreePanel
          class="skill-resource-workbench__tree-files"
          :busy="Boolean(fileMutation) || savingAll"
          :nodes="tree"
          :readonly="readonly"
          :current-path="activePanel === 'resources' ? selectedPath : ''"
          :dirty-paths="dirtyPaths"
          :error-counts="errorCounts"
          :warning-counts="warningCounts"
          @select="selectFileFromTree($event, close)"
          @create="openCreateFile"
          @download="downloadNode"
          @remove="removeNode"
          @rename="renameNode"
          @upload="chooseUpload"
        />
        <button
          v-if="capabilityAvailable"
          type="button"
          class="skill-resource-workbench__capability-entry"
          :class="{ 'is-active': activePanel === 'capability' }"
          :aria-current="activePanel === 'capability' ? 'page' : undefined"
          @click="selectCapabilityPanel(close)"
        >
          <span>
            <Connection aria-hidden="true" />
            能力绑定
          </span>
          <small>{{ capabilityCount }}</small>
        </button>
      </div>
      <input
        ref="uploadInputRef"
        type="file"
        class="skill-resource-workbench__hidden"
        @change="handleUpload"
      />
    </template>

    <template v-if="activePanel === 'resources'" #toolbar>
      <div class="skill-resource-workbench__toolbar">
        <div class="skill-resource-workbench__file-identity">
          <strong>{{
            currentFile?.path || currentNode?.path || '选择文件'
          }}</strong>
          <ElTag
            v-if="currentBuffer?.conflict"
            type="warning"
            size="small"
            effect="plain"
          >
            远端已变化
          </ElTag>
          <ElTag
            v-if="isMarkdown && isLargeForLive"
            type="info"
            size="small"
            effect="plain"
          >
            大文件 · 源码模式
          </ElTag>
          <ElTag
            v-else-if="isLargeForHighlight"
            type="info"
            size="small"
            effect="plain"
          >
            大文件 · 已关闭高亮
          </ElTag>
          <ElTag v-if="fileMutation" type="info" size="small" effect="plain">
            文件处理中
          </ElTag>
        </div>
        <ElSegmented
          v-if="isMarkdown"
          v-model="markdownMode"
          :options="markdownModeOptions"
          size="small"
        />
        <div class="skill-resource-workbench__toolbar-actions">
          <ElButton
            v-if="currentBuffer?.conflict"
            text
            type="warning"
            :loading="fileMutation === 'reload'"
            :disabled="Boolean(fileMutation) || savingAll"
            @click="reloadCurrent"
          >
            重新加载
          </ElButton>
          <ElButton
            v-if="
              currentFile &&
              !isBinaryResource &&
              (!isMarkdown || markdownMode === 'source') &&
              !(isScript && readonly)
            "
            :icon="Search"
            text
            aria-label="搜索"
            title="搜索"
            @click="openCurrentSearch"
          />
          <ElButton
            v-if="
              currentFile &&
              !isBinaryResource &&
              (!isMarkdown || markdownMode === 'source') &&
              !(isScript && readonly)
            "
            :icon="Fold"
            text
            aria-label="切换换行"
            title="切换换行"
            @click="lineWrapping = !lineWrapping"
          />
          <ElButton
            v-if="currentFile && isScript && !readonly"
            :icon="CopyDocument"
            text
            aria-label="复制脚本"
            title="复制"
            @click="copyCurrent"
          />
          <ElButton
            v-if="currentFile && (isBinaryResource || (isScript && !readonly))"
            :icon="Download"
            text
            :aria-label="isBinaryResource ? '下载资源' : '下载脚本'"
            title="下载"
            @click="downloadCurrent"
          />
          <ElButton
            v-if="currentFile && !(isScript && readonly)"
            :icon="FullScreen"
            text
            :aria-label="fullscreen ? '退出全屏' : '进入全屏'"
            :aria-pressed="fullscreen"
            :title="fullscreen ? '退出全屏' : '全屏'"
            @click="fullscreen = !fullscreen"
          />
        </div>
      </div>
    </template>

    <div
      v-show="activePanel === 'capability'"
      class="skill-resource-workbench__capability-area"
    >
      <slot name="capability"></slot>
    </div>
    <div
      v-show="activePanel === 'resources'"
      v-loading="loading"
      class="skill-resource-workbench__editor-area"
    >
      <template v-if="currentFile">
        <MarkdownLiveEditor
          v-if="isMarkdown && markdownMode === 'live' && !isLargeForLive"
          v-model="liveMarkdownBody"
          class="skill-resource-workbench__editor"
          :readonly="readonly"
          :resolve-image-url="resolveMarkdownImage"
          :upload-image="isNew || readonly ? undefined : uploadInlineImage"
          @error="handleEditorError"
          @fidelity-loss="handleMarkdownFidelityLoss"
          @frontmatter-activate="handleFrontmatterActivate"
          @link-click="handleRelativeLink"
          @save="emit('requestSave')"
        />
        <MarkdownSourceEditor
          v-else-if="isMarkdown"
          ref="sourceEditorRef"
          v-model="currentContent"
          class="skill-resource-workbench__editor"
          :highlight="!isLargeForHighlight"
          :line-wrapping="lineWrapping"
          :readonly="readonly"
          @cursor-change="cursor = $event"
          @save="emit('requestSave')"
        />
        <CodeViewer
          v-else-if="!isMarkdown && currentFile.isText && readonly"
          :content="currentContent"
          :filename="currentFile.path"
          :hash="currentFile.contentHash"
          :highlight="!isLargeForHighlight"
          :language="editorLanguage"
          :size="currentFile.size"
          download-mode="emit"
          @download="downloadCurrent"
          @error="handleEditorError"
        />
        <CodeEditor
          v-else-if="isScript || currentFile.isText"
          ref="codeEditorRef"
          v-model="currentContent"
          class="skill-resource-workbench__editor"
          :line-wrapping="lineWrapping"
          :language="isLargeForHighlight ? 'text' : editorLanguage"
          :readonly="readonly"
          @cursor-change="cursor = $event"
          @save="emit('requestSave')"
        />
        <div
          v-else-if="isBinaryResource"
          class="skill-resource-workbench__asset"
        >
          <header class="skill-resource-workbench__asset-info">
            <strong>{{ currentFile.path.split('/').pop() }}</strong>
            <dl>
              <div>
                <dt>格式</dt>
                <dd>
                  {{ currentFile.mediaType || 'application/octet-stream' }}
                </dd>
              </div>
              <div>
                <dt>大小</dt>
                <dd>{{ formatBytes(currentFile.size || 0) }}</dd>
              </div>
              <div v-if="currentFile.contentHash">
                <dt>校验值</dt>
                <dd :title="currentFile.contentHash">
                  {{ currentFile.contentHash.slice(0, 16) }}
                </dd>
              </div>
            </dl>
          </header>
          <div
            v-if="isImageResource || isPdfResource"
            v-loading="assetPreviewLoading"
            class="skill-resource-workbench__asset-preview"
          >
            <img
              v-if="isImageResource && assetPreviewUrl"
              class="skill-resource-workbench__asset-image"
              :src="assetPreviewUrl"
              :alt="currentFile.path"
              @load="handleAssetRendered"
              @error="handleAssetRenderError"
            />
            <iframe
              v-else-if="isPdfResource && assetPreviewUrl"
              :src="assetPreviewUrl"
              :title="`${currentFile.path} PDF 预览`"
              referrerpolicy="no-referrer"
              sandbox=""
              @load="handleAssetRendered"
              @error="handleAssetRenderError"
            ></iframe>
            <div
              v-else-if="assetPreviewError"
              class="skill-resource-workbench__asset-state"
              role="alert"
            >
              <strong>预览加载失败</strong>
              <span>可下载后使用本地应用打开</span>
            </div>
            <span
              v-else
              class="skill-resource-workbench__asset-state"
              role="status"
            >
              正在加载预览
            </span>
          </div>
          <div v-else class="skill-resource-workbench__asset-state">
            <strong>此格式不支持在线预览</strong>
            <span>可下载后使用本地应用打开</span>
          </div>
          <ElButton type="primary" :icon="Download" @click="downloadCurrent">
            下载
          </ElButton>
        </div>
      </template>
      <div
        v-else-if="currentNode?.type === 'DIRECTORY'"
        class="skill-resource-workbench__directory-state"
      >
        <strong>{{ currentNode.name }}</strong>
        <span>此目录暂无内容</span>
        <div v-if="!readonly">
          <ElButton type="primary" @click="openCreateFile(currentNode)">
            新建文件
          </ElButton>
          <ElButton @click="chooseUpload(currentNode)">上传资源</ElButton>
        </div>
      </div>
      <ElEmpty v-else :description="loadError || '从左侧选择文件'">
        <ElButton v-if="loadError" @click="init">重新加载</ElButton>
      </ElEmpty>
    </div>

    <template #status>
      <EditorStatusBar
        v-if="activePanel === 'resources' && currentFile && !isBinaryResource"
        :column="cursor.column"
        :language="editorLanguage"
        :line="cursor.line"
        :lines="lineCount"
        :mode="isMarkdown && markdownMode === 'live' ? 'document' : 'code'"
        :save-state="currentBuffer?.saveState"
        :size-label="sizeLabel"
        :word-count="wordCount"
      />
    </template>

    <template #inspector>
      <ValidationPanel :issues="issues" @locate="locateIssue" />
    </template>
  </DocumentEditorWorkbench>

  <EasyFlowFormModal
    v-model:open="createFileDialogOpen"
    width="480px"
    :closable="fileMutation !== 'create'"
    :confirm-loading="fileMutation === 'create'"
    confirm-text="创建"
    :submitting="fileMutation === 'create'"
    title="新建文件"
    @confirm="submitCreateFile"
  >
    <ElForm
      ref="createFileFormRef"
      class="easyflow-modal-form easyflow-modal-form--compact"
      label-position="top"
      :model="createFileForm"
      :rules="createFileRules"
    >
      <ElAlert
        v-if="createFileError"
        :closable="false"
        :title="createFileError"
        type="error"
        show-icon
      />
      <ElFormItem label="所属目录" prop="directory">
        <ElSelect v-model="createFileForm.directory">
          <ElOption
            v-for="directory in directoryOptions"
            :key="directory.path"
            :label="directory.path"
            :value="directory.path"
          />
        </ElSelect>
      </ElFormItem>
      <ElFormItem label="文件名" prop="fileName">
        <ElInput
          ref="createFileNameRef"
          v-model="createFileForm.fileName"
          maxlength="128"
          placeholder="例如 guide.md"
          @keyup.enter="submitCreateFile"
        />
      </ElFormItem>
    </ElForm>
  </EasyFlowFormModal>
</template>

<style scoped>
.skill-resource-workbench {
  height: 100%;
  min-height: 0;
}

.skill-resource-workbench.is-fullscreen {
  position: fixed;
  inset: var(--space-4);
  z-index: var(--popup-z-index);
  box-shadow: var(--shadow-float);
}

.skill-resource-workbench__hidden {
  display: none;
}

.skill-resource-workbench__tree-layout {
  display: flex;
  height: 100%;
  min-height: 0;
  flex-direction: column;
}

.skill-resource-workbench__tree-files {
  flex: 1;
  min-height: 0;
}

.skill-resource-workbench__capability-entry {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  justify-content: space-between;
  min-height: 44px;
  padding: 0 var(--space-3);
  margin: var(--space-2);
  color: hsl(var(--text-muted));
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-control);
  transition:
    color var(--motion-duration-fast) var(--motion-ease-standard),
    background-color var(--motion-duration-fast) var(--motion-ease-standard);
}

.skill-resource-workbench__capability-entry > span {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  min-width: 0;
  font-weight: 600;
}

.skill-resource-workbench__capability-entry svg {
  width: 16px;
  height: 16px;
}

.skill-resource-workbench__capability-entry small {
  display: grid;
  min-width: 24px;
  height: 24px;
  place-items: center;
  color: hsl(var(--text-muted));
  background: hsl(var(--surface-contrast-soft));
  border-radius: var(--radius-pill);
}

.skill-resource-workbench__capability-entry:hover,
.skill-resource-workbench__capability-entry.is-active {
  color: hsl(var(--nav-item-active-foreground));
  background: hsl(var(--nav-item-active));
}

.skill-resource-workbench__capability-entry:focus-visible {
  outline: 2px solid hsl(var(--primary) / 72%);
  outline-offset: 1px;
}

.skill-resource-workbench__toolbar {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  min-height: 48px;
  padding: 0 var(--space-3);
}

.skill-resource-workbench__file-identity {
  display: flex;
  flex: 1;
  gap: var(--space-2);
  align-items: center;
  min-width: 0;
}

.skill-resource-workbench__file-identity strong {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.skill-resource-workbench__toolbar-actions {
  display: flex;
  gap: var(--space-1);
  align-items: center;
}

.skill-resource-workbench__editor-area,
.skill-resource-workbench__capability-area,
.skill-resource-workbench__editor {
  display: flex;
  flex: 1;
  min-width: 0;
  min-height: 0;
}

.skill-resource-workbench__capability-area {
  overflow: hidden;
}

.skill-resource-workbench__asset {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-4);
  align-items: center;
  min-width: 0;
  padding: var(--space-6);
  overflow: auto;
}

.skill-resource-workbench__asset-preview {
  display: grid;
  place-items: center;
  width: 100%;
  max-width: 960px;
  min-height: 280px;
  overflow: hidden;
  background: hsl(var(--surface-subtle));
  border: 1px solid hsl(var(--line-subtle));
  border-radius: var(--radius-toolbar);
}

.skill-resource-workbench__asset-image,
.skill-resource-workbench__asset-preview iframe {
  width: 100%;
  height: min(62vh, 640px);
  border: 0;
}

.skill-resource-workbench__asset-image {
  object-fit: contain;
}

.skill-resource-workbench__asset-info {
  width: 100%;
  max-width: 960px;
  padding: var(--space-4);
  background: hsl(var(--surface-subtle));
  border: 1px solid hsl(var(--line-subtle));
  border-radius: var(--radius-toolbar);
}

.skill-resource-workbench__asset-info strong {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  color: hsl(var(--foreground));
  white-space: nowrap;
}

.skill-resource-workbench__asset-info dl {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
  margin: var(--space-2) 0 0;
}

.skill-resource-workbench__asset-info dl > div {
  display: flex;
  gap: var(--space-2);
  min-width: 0;
}

.skill-resource-workbench__asset-info dt,
.skill-resource-workbench__asset-info dd {
  margin: 0;
  font-size: 12px;
}

.skill-resource-workbench__asset-info dt {
  color: hsl(var(--text-muted));
}

.skill-resource-workbench__asset-info dd {
  max-width: 320px;
  overflow: hidden;
  text-overflow: ellipsis;
  color: hsl(var(--foreground));
  white-space: nowrap;
}

.skill-resource-workbench__asset-state {
  display: grid;
  gap: var(--space-2);
  place-items: center;
  min-height: 160px;
  color: hsl(var(--text-muted));
  text-align: center;
}

.skill-resource-workbench__asset-state strong {
  color: hsl(var(--foreground));
}

.skill-resource-workbench__directory-state {
  display: grid;
  gap: var(--space-2);
  place-content: center;
  place-items: center;
  width: 100%;
  color: hsl(var(--text-muted));
  text-align: center;
}

.skill-resource-workbench__directory-state strong {
  color: hsl(var(--foreground));
  font-size: 16px;
}

.skill-resource-workbench__directory-state > div {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

@media (max-width: 720px) {
  .skill-resource-workbench__toolbar {
    flex-wrap: wrap;
    align-items: flex-end;
    padding-block: var(--space-2);
  }

  .skill-resource-workbench__file-identity {
    min-width: 60%;
  }

  .skill-resource-workbench__toolbar-actions {
    justify-content: flex-end;
    width: 100%;
  }
}
</style>
