// retoor <retoor@molodetz.nl>
export const meta = {
  name: 'job-service',
  description: 'Scaffold an async JobService (the zip/fork pattern): the JobService subclass, enqueue/status/download routes, the JobOut schema, main.py registration, Devii tools, JobPoller frontend, and docs, then verify and write the integration tests (enqueue, status, download) in the api tier',
  phases: [
    { title: 'Understand', detail: 'read ZipService and ForkService as the template' },
    { title: 'Plan', detail: 'a per-touchpoint plan for the new job kind' },
    { title: 'Implement', detail: 'build the service and all consumers in the repo' },
    { title: 'Verify', detail: 'completeness, security, and audit-log review' },
    { title: 'Fix', detail: 'close gaps from the review' },
    { title: 'Test', detail: 'write the api-tier integration tests for enqueue, status, and download' },
  ],
}

const RULES = [
  'Obey DevPlace hard rules while editing:',
  '- No comments or docstrings in source except the file header and @tool docstrings. New files start with the "retoor <retoor@molodetz.nl>" header.',
  '- No em-dash characters; use a hyphen. Full Python type hints; pathlib over os; Pydantic input with max lengths.',
  '- Runtime artifacts live in config.DATA_DIR (the var/ dir), OUTSIDE the devplacepy package and NOT under /static. Heavy compression or blocking work runs in a subprocess. SQLite stays synchronous.',
  '- Enqueue endpoints own authz (require_user plus any resource guard); status and download are capability URLs scoped only by the unguessable uuid7. Soft-delete the job tracking rows; permanent artifacts are not deleted by cleanup().',
  '- Record audit events with record_system in the service. Frontend status polling uses JobPoller, never a bespoke loop.',
  '- Validate with the per-language checks (py_compile + pyflakes for Python, node --check for JS, brace balance for CSS, tag and {% %} balance for templates) and "python -c \\"from devplacepy.main import app\\"". NEVER run the test suite. Never perform any git write.',
].join('\n')

const CHECKLIST = [
  'A new async job kind must wire all of these (mirror ZipService/ForkService):',
  '1. services/jobs/{kind}_service.py - subclass JobService, set kind, implement async process(self, job) -> dict and cleanup(self, job).',
  '2. main.py - register the service via service_manager.register(...).',
  '3. routers/{area}.py - an enqueue route (guarded) calling queue.enqueue(kind=...), a GET status route returning a *JobOut, and a download/result route (FileResponse capability URL) where applicable.',
  '4. schemas.py - the *JobOut model with every key the status JSON returns.',
  '5. services/devii/actions/catalog.py - Devii tools for enqueue and status.',
  '6. docs_api.py - endpoint() entries for the enqueue, status, and download routes.',
  '7. static/js - wire JobPoller.run(statusUrl, {onDone, onFailed, onTimeout}) on the triggering element.',
  '8. CLI (optional) - a prune/clear subcommand if artifacts accumulate.',
  '9. README.md + devplacepy/services/jobs/CLAUDE.md - document the new job kind.',
].join('\n')

const TESTS = [
  'DevPlace test standard (a hard project requirement - one test file per endpoint, the directory tree mirroring the URL path):',
  'A job kind is exercised over HTTP, so its tests live in tests/api/ against the live uvicorn subprocess (app_server/seeded_db, requests/httpx vs BASE_URL), one file per route path (POST /projects/{slug}/zip -> tests/api/projects/zip.py; GET /zips/{uid} -> tests/api/zips/index.py). Cover enqueue (authz + a job uid back), status (the *JobOut shape and lifecycle), and download/result (the capability URL) where applicable.',
  'Because the service loop only runs in the lock owner and tests set DEVPLACE_DISABLE_SERVICES=1, assert the enqueue contract and the pending/known status shape rather than waiting on real completion; if you need a finished job, drive process() directly in a unit test under tests/unit/services/jobs/.',
  'Required patterns: scoped assertions; try/finally restore of any flipped global setting; the shared fixtures; raw inserts into a soft-delete table set deleted_at/deleted_by. Validate by a clean import only. NEVER run the suite.',
].join('\n')

function jobBrief() {
  if (!args) return ''
  if (typeof args === 'string') return args
  if (typeof args.description === 'string') return args.description
  return JSON.stringify(args)
}

const ask = jobBrief()
if (!ask) {
  log('No job description provided. Invoke as /job-service <what heavy work to run off the request path>.')
  return { error: 'no description provided' }
}

const MAP_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['summary'],
  properties: {
    summary: { type: 'string' },
    template: { type: 'string' },
    files: { type: 'array', items: { type: 'string' } },
  },
}

const PLAN_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['steps'],
  properties: {
    steps: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        required: ['file', 'change'],
        properties: { file: { type: 'string' }, change: { type: 'string' } },
      },
    },
  },
}

const BUILD_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['summary', 'filesChanged', 'validatorPassed'],
  properties: {
    summary: { type: 'string' },
    kind: { type: 'string' },
    filesChanged: { type: 'array', items: { type: 'string' } },
    validatorPassed: { type: 'boolean' },
    importOk: { type: 'boolean' },
  },
}

const FINDINGS_SCHEMA = {
  type: 'object',
  additionalProperties: false,
  required: ['summary', 'findings'],
  properties: {
    summary: { type: 'string' },
    findings: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        required: ['severity', 'file', 'rule', 'message'],
        properties: {
          severity: { type: 'string', enum: ['error', 'warning', 'info'] },
          file: { type: 'string' },
          line: { type: 'integer' },
          rule: { type: 'string' },
          message: { type: 'string' },
        },
      },
    },
  },
}

log(`Job service: ${ask}`)

const map = await agent(
  `Read the DevPlace async job framework and the two existing consumers ZipService and ForkService end to end (services/jobs/, the enqueue/status/download routes, their *JobOut schemas, Devii tools, and frontend pollers) as the template for a new job kind. Do not write anything.\n\nJob request: ${ask}\n\n${CHECKLIST}`,
  { agentType: 'Explore', label: 'understand', phase: 'Understand', schema: MAP_SCHEMA }
)

const plan = await agent(
  `Produce a per-file plan to add this new job kind, mirroring ZipService/ForkService across the checklist. One step per file. Do not write code.\n\nJob request: ${ask}\n\nTemplate map:\n${JSON.stringify(map, null, 2)}\n\n${CHECKLIST}`,
  { agentType: 'Plan', label: 'plan', phase: 'Plan', schema: PLAN_SCHEMA }
)

const build = await agent(
  `Implement this new async job kind coherently, editing files directly in the repo, mirroring ZipService/ForkService and following the plan. Keep the *JobOut schema, routes, Devii tools, and docs in agreement. Then run the per-language checks (py_compile + pyflakes for Python, node --check for JS, brace balance for CSS, tag and {% %} balance for templates) and "python -c \\"from devplacepy.main import app\\"". Do not write tests here. Do not run the suite. Do not commit.\n\nJob request: ${ask}\n\nPlan:\n${JSON.stringify(plan, null, 2)}\n\n${CHECKLIST}\n\n${RULES}\n\nReturn the job kind, files changed, and whether the checks and the import passed.`,
  { label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA }
)

const changed = (build && build.filesChanged) || []
const scopeNote = changed.length ? `\n\nRestrict findings to these files:\n${changed.join('\n')}` : ''

const audits = await parallel(
  [
    { key: 'fanout', agent: 'fanout-maintainer' },
    { key: 'security', agent: 'security-maintainer' },
    { key: 'audit', agent: 'audit-maintainer' },
    { key: 'docs', agent: 'docs-maintainer' },
  ].map((a) => () =>
    agent(
      `Operate in REPORT mode (read-only). Audit the new async job kind for your single dimension.${scopeNote}\n\nJob request: ${ask}`,
      { agentType: a.agent, label: `verify:${a.key}`, phase: 'Verify', schema: FINDINGS_SCHEMA }
    ).then((r) => ({ key: a.key, findings: (r && r.findings) || [] }))
  )
)

const gaps = audits
  .filter(Boolean)
  .flatMap((r) => r.findings.map((f) => ({ dimension: r.key, ...f })))
  .filter((f) => f.severity !== 'info')

let gapFix = 'no actionable gaps'
if (gaps.length) {
  gapFix = await agent(
    `Close these job-service gaps with minimal root-cause fixes in the repo, then re-run the per-language checks. Do not run the suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`,
    { label: 'fix-gaps', phase: 'Fix' }
  )
}

const tests = await agent(
  `Operate in FIX mode. Write the integration tests for the new job kind (enqueue, status, download) following the required patterns and the directory-mirrors-path layout. The job kind is not complete until each of its routes has a test. Create any missing package directories the test paths need. Validate by a clean import only. NEVER run the suite.\n\n${TESTS}\n\nJob request: ${ask}\nKind: ${build && build.kind}\nFiles changed:\n${changed.join('\n')}\n\nReturn the test files written and the routes they cover.`,
  { agentType: 'test-maintainer', label: 'tests', phase: 'Test' }
)

return { ask, map, plan, build, audit: gaps, gapFix, tests }
