import { existsSync, readFileSync } from 'node:fs'

const requiredFiles = [
  'src/app/App.tsx',
  'src/pages/login.tsx',
  'src/pages/workspace-body.tsx',
  'src/pages/workspace-home.tsx',
  'src/pages/workspace-explore.tsx',
  'src/pages/projects/project-overview.tsx',
  'src/pages/projects/project-inbox.tsx',
  'src/pages/projects/project-channel-page.tsx',
  'src/pages/projects/project-tasks.tsx',
  'src/pages/projects/project-ai.tsx',
  'src/pages/projects/project-notes.tsx',
  'src/pages/projects/project-cron.tsx',
  'src/pages/projects/project-new-channel.tsx',
  'src/pages/projects/project-rail.tsx',
  'src/pages/projects/project-sidebar.tsx',
  'src/pages/projects/project-topbar.tsx',
  'src/pages/projects/project-statusbar.tsx',
  'src/pages/projects/project-types.ts',
  'src/api/client.ts',
  'src/api/projects.ts',
  'src/api/mappers.tsx',
  'src/api/search.ts',
  'src/api/inbox.ts',
  'src/api/ai.ts',
  'scripts/api-client.test.mjs',
]

const failures = requiredFiles.filter((file) => !existsSync(file)).map((file) => `missing ${file}`)

const html = readFileSync('index.html', 'utf8')
if (!html.includes('<html lang="zh-CN">')) failures.push('index language must be zh-CN')
if (!html.includes('<title>森林AI</title>')) failures.push('document title must be 森林AI')
if (!html.includes('href="/senlinai-icon.svg"')) failures.push('favicon must use /senlinai-icon.svg')
if (!existsSync('public/senlinai-icon.svg')) failures.push('missing public/senlinai-icon.svg')

for (const file of ['src/pages/login.tsx', 'src/pages/projects/project-topbar.tsx']) {
  const source = readFileSync(file, 'utf8')
  if (!source.includes('/senlinai-icon.svg')) failures.push(`${file} must use the brand icon`)
}

for (const forbidden of ['森林Agent', 'SenlinAI Workbench']) {
  for (const file of requiredFiles.filter((name) => name.endsWith('.tsx'))) {
    if (readFileSync(file, 'utf8').includes(forbidden)) failures.push(`${file} contains legacy product name ${forbidden}`)
  }
}

const projectRailSource = readFileSync('src/pages/projects/project-rail.tsx', 'utf8')
if (!projectRailSource.includes('projectNameLabel(project.name)')) {
  failures.push('project rail labels must derive from the project name')
}
if (!readFileSync('src/pages/login.tsx', 'utf8').includes('v1.0 完整版')) {
  failures.push('login must identify the frontend as v1.0 完整版')
}
if (!projectRailSource.includes('icon={<IconDashboard />}') || projectRailSource.includes('rail-brand-icon')) {
  failures.push('workspace rail entry must use the dashboard icon instead of the brand logo')
}

const apiFiles = ['client.ts', 'projects.ts', 'mappers.tsx', 'search.ts', 'inbox.ts', 'ai.ts'].map((name) => `src/api/${name}`)
const apiSource = apiFiles.map((file) => readFileSync(file, 'utf8')).join('\n')
for (const forbidden of [
  { pattern: /\bID\s*\?\s*:/, label: 'optional PascalCase ID compatibility field' },
  { pattern: /\bName\s*\?\s*:/, label: 'optional PascalCase Name compatibility field' },
  { pattern: /['"`]\/api\/projects/, label: 'legacy /api/projects path' },
  {
    pattern: /\b(?:project|task|inbox)Id\s*\??\s*:\s*(?:string\s*\|\s*)?number\b/i,
    label: 'numeric project/task/inbox identity',
  },
]) {
  if (forbidden.pattern.test(apiSource)) failures.push(`src/api contains ${forbidden.label}`)
}

const clientSource = readFileSync('src/api/client.ts', 'utf8')
if (!clientSource.includes("'/api/v1/auth/login'")) failures.push('login must use /api/v1/auth/login')
if (!clientSource.includes('export class ApiError extends Error')) failures.push('client must export ApiError')

const projectsSource = readFileSync('src/api/projects.ts', 'utf8')
if (!projectsSource.includes("'/api/v1/projects'")) failures.push('project API must use /api/v1/projects')
if (/['"`]\/api\/(?!v1\/)/.test(apiSource)) failures.push('src/api paths must use the /api/v1 prefix')

const mappersSource = readFileSync('src/api/mappers.tsx', 'utf8')
if (!mappersSource.includes("filter((channel) => channel.type !== 'inbox')")) {
  failures.push('project channels must hide the removed Inbox message stream')
}

for (const match of apiSource.matchAll(/export type (\w+DTO)\s*=\s*\{([\s\S]*?)\n\}/g)) {
  const [, name, body] = match
  if (/^\s*\w+\s*\?\s*:/m.test(body)) failures.push(`${name} fields must be required`)
  if (/^\s*[A-Z]\w*\s*:/m.test(body)) failures.push(`${name} fields must use camelCase`)
  if (/^\s*(?:id|\w+Id)\s*:\s*number\b/im.test(body)) failures.push(`${name} identities must be strings`)
}
if (/\|\s*undefined/.test(projectsSource)) failures.push('project DTOs must not use undefined fields')

const searchSource = readFileSync('src/api/search.ts', 'utf8')
if (!searchSource.includes("'/api/v1/search'")) failures.push('search API must use /api/v1/search')
for (const unsupportedType of ["'source'", "'inbox'", "'ai_session'"]) {
  if (searchSource.includes(unsupportedType)) failures.push(`search DTO contains unsupported result type ${unsupportedType}`)
}

const loginSource = readFileSync('src/pages/login.tsx', 'utf8')
if (!loginSource.includes("import { normalizeBaseUrl } from '../api/client'")) failures.push('login must share the API base URL normalizer')
if (loginSource.includes('function normalizeBaseUrl')) failures.push('login must not define a second base URL normalizer')

const inboxSource = readFileSync('src/api/inbox.ts', 'utf8')
for (const path of ['/api/v1/projects/', '/api/v1/inbox/']) {
  if (!inboxSource.includes(path)) failures.push(`inbox API must use ${path}`)
}
if (inboxSource.includes('body: { suggestions }')) failures.push('inbox confirm must not send client-authored suggestion content')
if (!inboxSource.includes('body: { suggestionIds }')) failures.push('inbox confirm must send only saved suggestion identities')

const inboxPageSource = readFileSync('src/pages/projects/project-inbox.tsx', 'utf8')
if (!inboxPageSource.includes('确认创建')) failures.push('project inbox must expose the single confirmation action')

const aiApiSource = readFileSync('src/api/ai.ts', 'utf8')
for (const required of ['/api/v1/projects/', '/ai-sessions', '/api/v1/ai-experts', 'listAIExperts', 'listAISessions', 'createAISession']) {
  if (!aiApiSource.includes(required)) failures.push(`AI API must include ${required}`)
}
if (!aiApiSource.includes('signal?: AbortSignal')) failures.push('AI API requests must accept an AbortSignal')
if (!aiApiSource.includes('signal,')) failures.push('AI API requests must pass the AbortSignal to apiRequest')
if (/\b(?:task|note|source)Id\b/.test(aiApiSource)) failures.push('AI session responses must not expose automatic formal object IDs')

const aiPageSource = readFileSync('src/pages/projects/project-ai.tsx', 'utf8')
for (const required of ['新建会话', '选择专家进行会话', 'agent-expert-picker', 'selectedExpertID', 'expertId', 'agent-chat-shell', 'agent-composer', 'IconAttachment', 'agent-send-button', 'onPressEnter', 'loading', 'error']) {
  if (!aiPageSource.includes(required)) failures.push(`project AI page must include ${required}`)
}
for (const required of ['AbortController', 'generationRef', 'projectRef', 'setSessions([])']) {
  if (!aiPageSource.includes(required)) failures.push(`project AI page must gate stale requests with ${required}`)
}
if (aiPageSource.includes('overview-head') || aiPageSource.includes('<Title heading=')) {
  failures.push('project AI page must not render a title or subtitle header')
}
for (const forbidden of ['DeepSeek V4.0 Flash', '给 DeepSeek 发送消息']) {
  if (aiPageSource.includes(forbidden)) failures.push(`project AI page contains outdated or borrowed copy ${forbidden}`)
}

const tasksPageSource = readFileSync('src/pages/projects/project-tasks.tsx', 'utf8')
for (const required of ['selectedTag', 'setSelectedTag', 'visibleTasks', 'task-tag-filter', 'aria-pressed']) {
  if (!tasksPageSource.includes(required)) failures.push(`project tasks page must support tag filtering with ${required}`)
}

const explorePageSource = readFileSync('src/pages/workspace-explore.tsx', 'utf8')
for (const required of ['explore-source-card', 'explore-reader-layout', '同步数据源', '添加数据源', 'detectSource', 'explore-source-modal', 'openAddSource', 'openEditSource']) {
  if (!explorePageSource.includes(required)) failures.push(`workspace explore page must restore ${required}`)
}

const statusbarSource = readFileSync('src/pages/projects/project-statusbar.tsx', 'utf8')
for (const required of ['status-online-dot', 'status-upgrade', 'status-system', '个人资料', '升级套餐']) {
  if (!statusbarSource.includes(required)) failures.push(`project statusbar must restore ${required}`)
}

const newChannelSource = readFileSync('src/pages/projects/project-new-channel.tsx', 'utf8')
for (const required of ['保存频道', '频道名称', '频道类型', '链接地址', '创建后可接入的内容']) {
  if (!newChannelSource.includes(required)) failures.push(`new channel page must restore ${required}`)
}

const unsupportedControls = [
  { file: 'src/pages/projects/project-topbar.tsx', forbidden: ['停靠左边', '停靠右边', 'DockIcon', 'isDesktopRuntime'] },
]
for (const check of unsupportedControls) {
  const source = readFileSync(check.file, 'utf8')
  if (check.required && !source.includes(check.required)) failures.push(`${check.file} must show ${check.required}`)
  for (const forbidden of check.forbidden) {
    if (source.includes(forbidden)) failures.push(`${check.file} contains unsupported control ${forbidden}`)
  }
}

if (existsSync('src/App.tsx')) {
  failures.push('legacy src/App.tsx should be removed after page split')
}

for (const removed of ['src/pages/projects/project-data.tsx', 'src/pages/projects/project-inspector.tsx']) {
  if (existsSync(removed)) {
    failures.push(`${removed} should be removed; frontend data must come from backend APIs`)
  }
}

if (existsSync('src/app/App.tsx')) {
  const appSource = readFileSync('src/app/App.tsx', 'utf8')
  for (const forbidden of ['function LoginPage', 'function Workbench', 'function OverviewContent']) {
    if (appSource.includes(forbidden)) {
      failures.push(`src/app/App.tsx still contains ${forbidden}`)
    }
  }
}

if (failures.length) {
  console.error(failures.join('\n'))
  process.exit(1)
}

console.log('structure ok')
