import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { execFileSync } from 'node:child_process'
import path from 'node:path'
import type { APIRequestContext, Page, TestInfo } from '@playwright/test'

type CleanupTask = () => Promise<void>

export interface SeededNamespace {
  id: number
  slug: string
  displayName: string
  status?: string
  type?: string
  currentUserRole?: string
  canUnfreeze?: boolean
  canRestore?: boolean
  canDelete?: boolean
}

export interface SeededSkill {
  skillId: number
  namespace: string
  slug: string
  version: string
  status: string
}

export interface SeededReviewData {
  namespace: SeededNamespace
  skill: SeededSkill
}

interface ReviewTaskSummary {
  id: number
  namespace: string
  skillSlug: string
  status: string
  submittedBy: string
  version: string
}

interface NamespaceCandidate {
  userId: string
  displayName: string
  email?: string
  status: string
}

interface ApiEnvelope<T> {
  code: number
  msg: string
  data: T
}

interface ApiFailure extends Error {
  status?: number
  code?: number
}

const cleanupTimeoutMs = process.env.CI ? 8_000 : 5_000

export interface SeedSkillOptions {
  name?: string
  description?: string
  version?: string
  readmeHeading?: string
}

function asApiErrorBody(value: unknown): string {
  if (!value || typeof value !== 'object') {
    return ''
  }
  const maybe = value as { msg?: unknown }
  return typeof maybe.msg === 'string' ? maybe.msg : ''
}

function uniqueSuffix(testInfo?: TestInfo): string {
  const worker = testInfo?.parallelIndex ?? 0
  return `${worker}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
}

async function runCleanupTaskWithTimeout(task: CleanupTask): Promise<void> {
  await new Promise<void>((resolve, reject) => {
    const timeout = setTimeout(() => {
      reject(new Error(`cleanup task timed out after ${cleanupTimeoutMs}ms`))
    }, cleanupTimeoutMs)

    void task()
      .then(() => {
        clearTimeout(timeout)
        resolve()
      })
      .catch((error) => {
        clearTimeout(timeout)
        reject(error)
      })
  })
}

function buildSkillPackageContent(suffix: string, options?: SeedSkillOptions) {
  const skillName = (options?.name || `e2e-skill-${suffix}`).slice(0, 48)
  const description = options?.description || 'E2E generated skill for real-request tests'
  const version = options?.version || '1.0.0'
  const readmeHeading = options?.readmeHeading || skillName
  const skillMd = `---
name: ${skillName}
description: ${description}
version: ${version}
---

# ${readmeHeading}

Generated by Playwright E2E.
`

  return {
    readmeHeading,
    skillMd,
    skillName,
  }
}

function buildSkillPackageZipBuffer(suffix: string, options?: SeedSkillOptions): Buffer {
  const tempRoot = mkdtempSync(path.join(tmpdir(), 'skillhub-e2e-'))
  try {
    const packageDir = path.join(tempRoot, `pkg-${suffix}`)
    const zipPath = path.join(tempRoot, `pkg-${suffix}.zip`)
    const { readmeHeading, skillMd } = buildSkillPackageContent(suffix, options)

    execFileSync('mkdir', ['-p', packageDir])
    writeFileSync(path.join(packageDir, 'SKILL.md'), skillMd, 'utf8')
    writeFileSync(path.join(packageDir, 'README.md'), `# ${readmeHeading}\n`, 'utf8')
    execFileSync('zip', ['-q', '-r', zipPath, 'SKILL.md', 'README.md'], { cwd: packageDir })
    return readFileSync(zipPath)
  } finally {
    rmSync(tempRoot, { recursive: true, force: true })
  }
}

function createSkillPackageZipFile(suffix: string, options?: SeedSkillOptions): { filePath: string; cleanup: () => void } {
  const tempRoot = mkdtempSync(path.join(tmpdir(), 'skillhub-e2e-file-'))
  const packageDir = path.join(tempRoot, `pkg-${suffix}`)
  const zipPath = path.join(tempRoot, `pkg-${suffix}.zip`)
  const { readmeHeading, skillMd } = buildSkillPackageContent(suffix, options)

  execFileSync('mkdir', ['-p', packageDir])
  writeFileSync(path.join(packageDir, 'SKILL.md'), skillMd, 'utf8')
  writeFileSync(path.join(packageDir, 'README.md'), `# ${readmeHeading}\n`, 'utf8')
  execFileSync('zip', ['-q', '-r', zipPath, 'SKILL.md', 'README.md'], { cwd: packageDir })

  return {
    filePath: zipPath,
    cleanup: () => {
      rmSync(tempRoot, { recursive: true, force: true })
    },
  }
}

async function parseEnvelope<T>(response: Awaited<ReturnType<APIRequestContext['fetch']>>): Promise<T> {
  const text = await response.text()
  let parsed: ApiEnvelope<T> | null = null
  try {
    parsed = JSON.parse(text) as ApiEnvelope<T>
  } catch {
    throw new Error(`Non-JSON response: status=${response.status()} body=${text.slice(0, 200)}`)
  }

  if (!response.ok() || parsed.code !== 0) {
    const error = new Error(
      `API failed: status=${response.status()} code=${parsed.code} msg=${asApiErrorBody(parsed) || parsed.msg}`,
    ) as ApiFailure
    error.status = response.status()
    error.code = parsed.code
    throw error
  }
  return parsed.data
}

export class E2eTestDataBuilder {
  private readonly request: APIRequestContext
  private readonly suffix: string
  private readonly cleanupTasks: CleanupTask[] = []
  private ensuredNamespace?: SeededNamespace

  constructor(
    private readonly page: Page,
    testInfo?: TestInfo,
  ) {
    this.request = page.context().request
    this.suffix = uniqueSuffix(testInfo)
  }

  async init(): Promise<void> {
    // Prime CSRF/session cookie path used by write endpoints.
    await this.request.get('/api/v1/auth/providers')
  }

  async cleanup(): Promise<void> {
    for (let i = this.cleanupTasks.length - 1; i >= 0; i -= 1) {
      try {
        await runCleanupTaskWithTimeout(this.cleanupTasks[i])
      } catch {
        // Best-effort cleanup for E2E environments.
      }
    }
  }

  async createNamespace(base = 'e2e-team'): Promise<SeededNamespace> {
    const rawSlug = `${base}-${this.suffix}`
      .toLowerCase()
      .replace(/[^a-z0-9-]/g, '-')
      .replace(/-+/g, '-')
      .replace(/^-+|-+$/g, '')
    const slug = rawSlug.slice(0, 64)
    const displayName = `E2E ${slug}`

    const created = await parseEnvelope<SeededNamespace>(
      await this.request.post('/api/v1/namespaces', {
        data: {
          slug,
          displayName,
          description: `E2E namespace ${slug}`,
        },
      }),
    )

    this.cleanupTasks.push(async () => {
      await this.request.post(`/api/web/namespaces/${encodeURIComponent(created.slug)}/archive`, {
        data: { reason: 'e2e cleanup' },
      })
    })

    return created
  }

  async listMyNamespaces(): Promise<SeededNamespace[]> {
    return parseEnvelope<SeededNamespace[]>(
      await this.request.get('/api/web/me/namespaces'),
    )
  }

  private isTeamNamespace(namespace: SeededNamespace): boolean {
    return namespace.type === 'TEAM' || namespace.slug !== 'global'
  }

  private isActiveNamespace(namespace: SeededNamespace): boolean {
    return namespace.status === 'ACTIVE'
  }

  private async activateNamespace(namespace: SeededNamespace): Promise<SeededNamespace | null> {
    if (!this.isTeamNamespace(namespace)) {
      return null
    }

    if (namespace.status === 'FROZEN' && namespace.canUnfreeze) {
      return parseEnvelope<SeededNamespace>(
        await this.request.post(`/api/web/namespaces/${encodeURIComponent(namespace.slug)}/unfreeze`),
      )
    }

    if (namespace.status === 'ARCHIVED' && namespace.canRestore) {
      return parseEnvelope<SeededNamespace>(
        await this.request.post(`/api/web/namespaces/${encodeURIComponent(namespace.slug)}/restore`),
      )
    }

    return null
  }

  async ensureWritableNamespace(): Promise<SeededNamespace> {
    if (this.ensuredNamespace) {
      return this.ensuredNamespace
    }

    try {
      const created = await this.createNamespace('e2e-team')
      this.ensuredNamespace = created
      return created
    } catch (error) {
      const failure = error as ApiFailure
      if (failure.status !== 403) {
        throw error
      }
    }

    const namespaces = await this.listMyNamespaces()
    const activeTeam = namespaces.find((item) => this.isTeamNamespace(item) && this.isActiveNamespace(item))
    if (activeTeam) {
      this.ensuredNamespace = activeTeam
      return activeTeam
    }

    const activeFallback = namespaces.find((item) => this.isActiveNamespace(item))
    if (activeFallback) {
      this.ensuredNamespace = activeFallback
      return activeFallback
    }

    const activatable = namespaces.find((item) =>
      this.isTeamNamespace(item)
      && ((item.status === 'FROZEN' && item.canUnfreeze) || (item.status === 'ARCHIVED' && item.canRestore)),
    )
    if (activatable) {
      const activated = await this.activateNamespace(activatable)
      if (activated) {
        this.ensuredNamespace = activated
        return activated
      }
    }

    const summary = namespaces
      .map((item) => `${item.slug}:${item.status ?? 'UNKNOWN'}`)
      .join(', ')
    throw new Error(`No active writable namespace available for e2e data seeding [${summary}]`)
  }

  async ensureReviewableNamespace(): Promise<SeededNamespace> {
    if (this.ensuredNamespace && this.isTeamNamespace(this.ensuredNamespace) && this.isActiveNamespace(this.ensuredNamespace)) {
      return this.ensuredNamespace
    }

    try {
      const created = await this.createNamespace('e2e-team')
      this.ensuredNamespace = created
      return created
    } catch (error) {
      const failure = error as ApiFailure
      if (failure.status !== 403) {
        throw error
      }
    }

    const namespaces = await this.listMyNamespaces()
    const activeTeam = namespaces.find((item) => this.isTeamNamespace(item) && this.isActiveNamespace(item))
    if (activeTeam) {
      this.ensuredNamespace = activeTeam
      return activeTeam
    }

    const activatable = namespaces.find((item) =>
      this.isTeamNamespace(item)
      && ((item.status === 'FROZEN' && item.canUnfreeze) || (item.status === 'ARCHIVED' && item.canRestore)),
    )
    if (activatable) {
      const activated = await this.activateNamespace(activatable)
      if (activated) {
        this.ensuredNamespace = activated
        return activated
      }
    }

    const summary = namespaces
      .map((item) => `${item.slug}:${item.status ?? 'UNKNOWN'}`)
      .join(', ')
    throw new Error(`No TEAM namespace available for review E2E data seeding [${summary}]`)
  }

  private async getMySkillInNamespace(namespaceSlug: string): Promise<SeededSkill | null> {
    const page = await parseEnvelope<{
      items: Array<{
        id: number
        namespace: string
        slug: string
        headlineVersion?: { version: string; status: string }
      }>
    }>(
      await this.request.get('/api/web/me/skills?page=0&size=50'),
    )

    const hit = page.items.find((item) => item.namespace === namespaceSlug)
    if (!hit || !hit.headlineVersion) {
      return null
    }

    return {
      skillId: hit.id,
      namespace: hit.namespace,
      slug: hit.slug,
      version: hit.headlineVersion.version,
      status: hit.headlineVersion.status,
    }
  }

  async waitForSearchResult(query: string, expectedSlug?: string): Promise<void> {
    const encodedQuery = encodeURIComponent(query)

    for (let attempt = 0; attempt < 20; attempt += 1) {
      try {
        const page = await parseEnvelope<{
          items: Array<{ slug: string }>
        }>(
          await this.request.get(`/api/web/skills?q=${encodedQuery}&sort=relevance&page=0&size=50`),
        )
        if (!expectedSlug || page.items.some((item) => item.slug === expectedSlug)) {
          return
        }
      } catch {
        // Search indexing can lag briefly behind publish in local environments.
      }

      await new Promise((resolve) => setTimeout(resolve, 300 * (attempt + 1)))
    }

    throw new Error(`Timed out waiting for search result "${query}"${expectedSlug ? ` (${expectedSlug})` : ''}`)
  }

  async waitForSearchResults(query: string, expectedSlugs: string[]): Promise<void> {
    const pending = new Set(expectedSlugs)
    if (pending.size === 0) {
      return
    }

    const encodedQuery = encodeURIComponent(query)

    for (let attempt = 0; attempt < 20; attempt += 1) {
      try {
        const page = await parseEnvelope<{
          items: Array<{ slug: string }>
        }>(
          await this.request.get(`/api/web/skills?q=${encodedQuery}&sort=relevance&page=0&size=50`),
        )

        for (const item of page.items) {
          pending.delete(item.slug)
        }

        if (pending.size === 0) {
          return
        }
      } catch {
        // Search indexing can lag briefly behind publish in local environments.
      }

      await new Promise((resolve) => setTimeout(resolve, 300 * (attempt + 1)))
    }

    throw new Error(`Timed out waiting for search results "${query}" (${Array.from(pending).join(', ')})`)
  }

  async waitForPendingReview(namespaceSlug: string, skillSlug: string, version: string): Promise<number> {
    for (let attempt = 0; attempt < 20; attempt += 1) {
      try {
        const page = await parseEnvelope<{
          items: ReviewTaskSummary[]
        }>(
          await this.request.get('/api/web/reviews?status=PENDING&page=0&size=100&sortDirection=DESC'),
        )

        const matched = page.items.find((item) =>
          item.namespace === namespaceSlug &&
          item.skillSlug === skillSlug &&
          item.version === version &&
          item.status === 'PENDING',
        )
        if (matched) {
          return matched.id
        }
      } catch {
        // Review list can lag behind publish very briefly.
      }

      await new Promise((resolve) => setTimeout(resolve, 300 * (attempt + 1)))
    }

    throw new Error(`Timed out waiting for pending review ${namespaceSlug}/${skillSlug}@${version}`)
  }

  async approveReview(reviewTaskId: number, comment = 'Approved by Playwright E2E'): Promise<void> {
    let lastError: unknown
    for (let attempt = 0; attempt < 30; attempt += 1) {
      try {
        await parseEnvelope<ReviewTaskSummary>(
          await this.request.post(`/api/web/reviews/${reviewTaskId}/approve`, {
            data: { comment },
          }),
        )
        return
      } catch (error) {
        lastError = error
        const message = error instanceof Error ? error.message : ''
        const isScanInProgress = message.includes('扫描') || message.toLowerCase().includes('scan is still in progress')
        if (!isScanInProgress) {
          throw error
        }
        await new Promise((resolve) => setTimeout(resolve, 500))
      }
    }
    throw lastError instanceof Error ? lastError : new Error('approveReview timed out')
  }

  async searchNamespaceMemberCandidates(slug: string, search: string): Promise<NamespaceCandidate[]> {
    const query = new URLSearchParams({ search })
    return parseEnvelope<NamespaceCandidate[]>(
      await this.request.get(`/api/web/namespaces/${encodeURIComponent(slug)}/member-candidates?${query.toString()}`),
    )
  }

  async addNamespaceMember(slug: string, userId: string, role: 'MEMBER' | 'ADMIN' | 'OWNER' = 'MEMBER'): Promise<void> {
    await parseEnvelope<{ userId: string; role: string }>(
      await this.request.post(`/api/web/namespaces/${encodeURIComponent(slug)}/members`, {
        data: { userId, role },
      }),
    )
  }

  async publishSkill(namespaceSlug: string, options?: SeedSkillOptions): Promise<SeededSkill> {
    const unique = `${this.suffix}_${Math.random().toString(36).slice(2, 6)}`
    const zipBuffer = buildSkillPackageZipBuffer(unique, options)

    const result = await parseEnvelope<SeededSkill>(
      await this.request.post(`/api/web/skills/${encodeURIComponent(namespaceSlug)}/publish`, {
        multipart: {
          file: {
            name: 'sample-skill.zip',
            mimeType: 'application/zip',
            buffer: zipBuffer,
          },
          visibility: 'PUBLIC',
        },
      }),
    )

    this.cleanupTasks.push(async () => {
      await this.request.delete(`/api/web/skills/${encodeURIComponent(result.namespace)}/${encodeURIComponent(result.slug)}`)
    })

    return result
  }

  createSkillPackageFile(options?: SeedSkillOptions): string {
    const unique = `${this.suffix}_${Math.random().toString(36).slice(2, 6)}`
    const { filePath, cleanup } = createSkillPackageZipFile(unique, options)
    this.cleanupTasks.push(async () => {
      cleanup()
    })
    return filePath
  }

  async createReviewData(): Promise<SeededReviewData> {
    const namespace = await this.ensureReviewableNamespace()
    const skill = await this.publishSkill(namespace.slug)
    return { namespace, skill }
  }
}
