import { Hype } from '@because/hype'
import { define, stylesToCSS } from '@because/forge'
import { baseStyles, on, ToolScript, theme } from '@because/toes/tools'
import { discoverCronJobs } from './lib/discovery'
import { scheduleJob, stopJob } from './lib/scheduler'
import { executeJob } from './lib/executor'
import { setJobs, setInvalidJobs, getJob, getAllJobs, getInvalidJobs, broadcast } from './lib/state'
import { SCHEDULES, type CronJob, type InvalidJob } from './lib/schedules'
import type { Child } from 'hono/jsx'
import { join } from 'path'
import { mkdir, writeFile } from 'fs/promises'
import { existsSync, watch } from 'fs'

const APPS_DIR = process.env.APPS_DIR!

const app = new Hype({ prettyHTML: false })

// Styles
const Container = define('Container', {
  fontFamily: theme('fonts-sans'),
  padding: '20px',
  paddingTop: 0,
  maxWidth: '900px',
  margin: '0 auto',
  color: theme('colors-text'),
})

const JobList = define('JobList', {
  listStyle: 'none',
  padding: 0,
  margin: '20px 0',
  border: `1px solid ${theme('colors-border')}`,
  borderRadius: theme('radius-md'),
  overflow: 'hidden',
})

const JobItem = define('JobItem', {
  padding: '12px 15px',
  borderBottom: `1px solid ${theme('colors-border')}`,
  display: 'flex',
  alignItems: 'center',
  gap: '15px',
  states: {
    ':last-child': { borderBottom: 'none' },
    ':hover': { backgroundColor: theme('colors-bgHover') },
  },
})

const StatusDot = define('StatusDot', {
  width: '10px',
  height: '10px',
  borderRadius: '50%',
  flexShrink: 0,
})

const JobName = define('JobName', {
  fontFamily: theme('fonts-mono'),
  fontSize: '14px',
  flex: 1,
})

const Schedule = define('Schedule', {
  fontSize: '13px',
  color: theme('colors-textMuted'),
  minWidth: '80px',
})

const Time = define('Time', {
  fontSize: '12px',
  color: theme('colors-textMuted'),
  minWidth: '100px',
})

const RunButton = define('RunButton', {
  base: 'button',
  padding: '4px 10px',
  marginTop: '10px',
  fontSize: '12px',
  backgroundColor: theme('colors-primary'),
  color: 'white',
  border: 'none',
  borderRadius: theme('radius-md'),
  cursor: 'pointer',
  states: {
    ':hover': { opacity: 0.9 },
    ':disabled': { opacity: 0.5, cursor: 'not-allowed' },
  },
})

const EmptyState = define('EmptyState', {
  padding: '40px 20px',
  textAlign: 'center',
  color: theme('colors-textMuted'),
})

const InvalidItem = define('InvalidItem', {
  padding: '12px 15px',
  borderBottom: `1px solid ${theme('colors-border')}`,
  display: 'flex',
  alignItems: 'center',
  gap: '15px',
  opacity: 0.7,
  states: {
    ':last-child': { borderBottom: 'none' },
  },
})

const ErrorText = define('ErrorText', {
  fontSize: '12px',
  color: theme('colors-error'),
  flex: 1,
})

const ActionRow = define('ActionRow', {
  display: 'flex',
  justifyContent: 'flex-end',
  marginBottom: '15px',
})

const NewButton = define('NewButton', {
  base: 'a',
  padding: '8px 16px',
  fontSize: '14px',
  backgroundColor: theme('colors-primary'),
  color: 'white',
  border: 'none',
  borderRadius: theme('radius-md'),
  cursor: 'pointer',
  textDecoration: 'none',
  display: 'inline-block',
  states: {
    ':hover': { opacity: 0.9 },
  },
})

const buttonStyles = {
  padding: '8px 16px',
  fontSize: '14px',
  backgroundColor: theme('colors-primary'),
  color: 'white',
  border: 'none',
  borderRadius: theme('radius-md'),
  cursor: 'pointer',
}

const Form = define('Form', {
  display: 'flex',
  flexDirection: 'column',
  gap: '16px',
  maxWidth: '400px',
})

const FormGroup = define('FormGroup', {
  display: 'flex',
  flexDirection: 'column',
  gap: '6px',
})

const Label = define('Label', {
  fontSize: '14px',
  fontWeight: 500,
})

const inputStyles = {
  padding: '8px 12px',
  fontSize: '14px',
  border: `1px solid ${theme('colors-border')}`,
  borderRadius: theme('radius-md'),
  backgroundColor: theme('colors-bg'),
  color: theme('colors-text'),
}

const ButtonRow = define('ButtonRow', {
  display: 'flex',
  gap: '10px',
  marginTop: '10px',
})

const CancelButton = define('CancelButton', {
  base: 'a',
  padding: '8px 16px',
  fontSize: '14px',
  backgroundColor: 'transparent',
  color: theme('colors-text'),
  border: `1px solid ${theme('colors-border')}`,
  borderRadius: theme('radius-md'),
  cursor: 'pointer',
  textDecoration: 'none',
  states: {
    ':hover': { backgroundColor: theme('colors-bgHover') },
  },
})

const BackLink = define('BackLink', {
  base: 'a',
  fontSize: '13px',
  color: theme('colors-textMuted'),
  textDecoration: 'none',
  states: {
    ':hover': { color: theme('colors-text') },
  },
})

const DetailHeader = define('DetailHeader', {
  display: 'flex',
  alignItems: 'center',
  gap: '12px',
  marginBottom: '20px',
})

const DetailTitle = define('DetailTitle', {
  base: 'h1',
  fontFamily: theme('fonts-mono'),
  fontSize: '18px',
  fontWeight: 600,
  margin: 0,
  flex: 1,
})

const DetailMeta = define('DetailMeta', {
  display: 'flex',
  gap: '20px',
  marginBottom: '20px',
  fontSize: '13px',
  color: theme('colors-textMuted'),
})

const MetaItem = define('MetaItem', {
  display: 'flex',
  gap: '6px',
})

const MetaLabel = define('MetaLabel', {
  fontWeight: 500,
  color: theme('colors-text'),
})

const OutputSection = define('OutputSection', {
  marginTop: '20px',
})

const OutputLabel = define('OutputLabel', {
  fontSize: '13px',
  fontWeight: 500,
  marginBottom: '8px',
})

const OutputBlock = define('OutputBlock', {
  base: 'pre',
  fontFamily: theme('fonts-mono'),
  fontSize: '12px',
  lineHeight: 1.5,
  padding: '12px',
  backgroundColor: theme('colors-bgElement'),
  border: `1px solid ${theme('colors-border')}`,
  borderRadius: theme('radius-md'),
  overflowX: 'auto',
  overflowY: 'auto',
  maxHeight: '60vh',
  whiteSpace: 'pre-wrap',
  wordBreak: 'break-word',
  margin: 0,
})

const ErrorBlock = define('ErrorBlock', {
  base: 'pre',
  fontFamily: theme('fonts-mono'),
  fontSize: '12px',
  lineHeight: 1.5,
  padding: '12px',
  backgroundColor: theme('colors-bgElement'),
  border: `1px solid ${theme('colors-error')}`,
  borderRadius: theme('radius-md'),
  color: theme('colors-error'),
  overflowX: 'auto',
  overflowY: 'auto',
  maxHeight: '60vh',
  whiteSpace: 'pre-wrap',
  wordBreak: 'break-word',
  margin: 0,
})

const StatusBadge = define('StatusBadge', {
  base: 'span',
  fontSize: '12px',
  padding: '2px 8px',
  borderRadius: '9999px',
  fontWeight: 500,
})

// Layout
function Layout({ title, children, refresh }: { title: string; children: Child; refresh?: boolean }) {
  return (
    <html>
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        {refresh && <meta http-equiv="refresh" content="2" />}
        <title>{title}</title>
        <link rel="stylesheet" href="/styles.css" />
      </head>
      <body>
        <ToolScript />
        <Container>
          {children}
        </Container>
      </body>
    </html>
  )
}

function formatRelative(ts?: number): string {
  if (!ts) return '-'
  const diff = Date.now() - ts
  if (diff < 0) {
    const mins = Math.round(-diff / 60000)
    if (mins < 60) return `in ${mins}m`
    const hours = Math.round(mins / 60)
    if (hours < 24) return `in ${hours}h`
    return `in ${Math.round(hours / 24)}d`
  }
  const mins = Math.round(diff / 60000)
  if (mins < 60) return `${mins}m ago`
  const hours = Math.round(mins / 60)
  if (hours < 24) return `${hours}h ago`
  return `${Math.round(hours / 24)}d ago`
}

function statusColor(job: CronJob): string {
  if (job.state === 'running') return theme('colors-statusRunning')
  if (job.state === 'disabled') return theme('colors-textMuted')
  if (job.lastExitCode !== undefined && job.lastExitCode !== 0) return theme('colors-error')
  return theme('colors-statusRunning')
}

// Routes
app.get('/ok', c => c.text('ok'))

app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, {
  'Content-Type': 'text/css; charset=utf-8',
}))

// JSON API
app.get('/api/jobs', c => {
  const appFilter = c.req.query('app')
  let jobs = getAllJobs()
  if (appFilter) jobs = jobs.filter(j => j.app === appFilter)
  jobs.sort((a, b) => a.id.localeCompare(b.id))
  return c.json(jobs.map(j => ({
    app: j.app,
    name: j.name,
    schedule: j.schedule,
    state: j.state,
    status: statusLabel(j),
    lastRun: j.lastRun,
    lastDuration: j.lastDuration,
    lastExitCode: j.lastExitCode,
    nextRun: j.nextRun,
  })))
})

app.get('/api/jobs/:app/:name', c => {
  const id = `${c.req.param('app')}:${c.req.param('name')}`
  const job = getJob(id)
  if (!job) return c.json({ error: 'Job not found' }, 404)
  return c.json({
    app: job.app,
    name: job.name,
    schedule: job.schedule,
    state: job.state,
    status: statusLabel(job),
    lastRun: job.lastRun,
    lastDuration: job.lastDuration,
    lastExitCode: job.lastExitCode,
    lastError: job.lastError,
    lastOutput: job.lastOutput,
    nextRun: job.nextRun,
  })
})

app.post('/api/jobs/:app/:name/run', async c => {
  const id = `${c.req.param('app')}:${c.req.param('name')}`
  const job = getJob(id)
  if (!job) return c.json({ error: 'Job not found' }, 404)
  if (job.state === 'running') return c.json({ error: 'Job is already running' }, 409)
  executeJob(job, broadcast)
  return c.json({ ok: true, message: `Started ${id}` })
})

app.get('/', async c => {
  const appFilter = c.req.query('app')
  let jobs = getAllJobs()
  let invalid = getInvalidJobs()

  if (appFilter) {
    jobs = jobs.filter(j => j.app === appFilter)
    invalid = invalid.filter(j => j.app === appFilter)
  }

  jobs.sort((a, b) => a.id.localeCompare(b.id))
  invalid.sort((a, b) => a.id.localeCompare(b.id))

  const hasAny = jobs.length > 0 || invalid.length > 0
  const anyRunning = jobs.some(j => j.state === 'running')

  return c.html(
    <Layout title="Cron Jobs" refresh={anyRunning}>
      <ActionRow>
        <NewButton href={`/new?app=${appFilter || ''}`}>New Job</NewButton>
      </ActionRow>
      {!hasAny ? (
        <EmptyState>
          No cron jobs found.
          <br />
          Create a cron/*.ts file in any app to get started.
        </EmptyState>
      ) : (
        <JobList>
          {jobs.map(job => (
            <JobItem>
              <StatusDot style={{ backgroundColor: statusColor(job) }} />
              <JobName>
                <a href={`/job/${job.app}/${job.name}${appFilter ? `?app=${appFilter}` : ''}`} style={{ color: 'inherit', textDecoration: 'none' }}>
                  {job.app}/{job.name}
                </a>
              </JobName>
              <Schedule>{job.schedule}</Schedule>
              <Time title="Last run">{formatRelative(job.lastRun)}</Time>
              <Time title="Next run">{formatRelative(job.nextRun)}</Time>
              <form method="post" action={`/run/${job.app}/${job.name}`}>
                <RunButton type="submit" disabled={job.state === 'running'}>
                  {job.state === 'running' ? 'Running...' : 'Run Now'}
                </RunButton>
              </form>
            </JobItem>
          ))}
          {invalid.map(job => (
            <InvalidItem>
              <StatusDot style={{ backgroundColor: theme('colors-error') }} />
              <JobName>{job.app}/{job.name}</JobName>
              <ErrorText>{job.error}</ErrorText>
            </InvalidItem>
          ))}
        </JobList>
      )}
    </Layout>
  )
})

function statusBadgeStyle(job: CronJob): Record<string, string> {
  if (job.state === 'running') return { backgroundColor: theme('colors-statusRunning'), color: 'white' }
  if (job.lastExitCode !== undefined && job.lastExitCode !== 0) return { backgroundColor: theme('colors-error'), color: 'white' }
  return { backgroundColor: theme('colors-bgElement'), color: theme('colors-textMuted') }
}

function statusLabel(job: CronJob): string {
  if (job.state === 'running') return 'running'
  if (job.lastExitCode !== undefined && job.lastExitCode !== 0) return `exit ${job.lastExitCode}`
  if (job.lastRun) return 'ok'
  return 'idle'
}

function formatDuration(ms?: number): string {
  if (!ms) return '-'
  if (ms < 1000) return `${ms}ms`
  if (ms < 60000) return `${Math.round(ms / 1000)}s`
  return `${Math.round(ms / 60000)}m`
}

app.get('/job/:app/:name', async c => {
  const id = `${c.req.param('app')}:${c.req.param('name')}`
  const job = getJob(id)
  const appFilter = c.req.query('app')
  const backUrl = appFilter ? `/?app=${appFilter}` : '/'

  if (!job) {
    return c.html(
      <Layout title="Job Not Found">
        <BackLink href={backUrl}>&#8592; Back</BackLink>
        <EmptyState>Job not found: {id}</EmptyState>
      </Layout>
    )
  }

  return c.html(
    <Layout title={`${job.app}/${job.name}`} refresh={job.state === 'running'}>
      <BackLink href={backUrl}>&#8592; Back</BackLink>
      <DetailHeader>
        <StatusDot style={{ backgroundColor: statusColor(job) }} />
        <DetailTitle>{job.app}/{job.name}</DetailTitle>
        <StatusBadge style={statusBadgeStyle(job)}>{statusLabel(job)}</StatusBadge>
        <form method="post" action={`/run/${job.app}/${job.name}?return=detail&app=${appFilter || ''}`}>
          <RunButton type="submit" disabled={job.state === 'running'}>
            {job.state === 'running' ? 'Running...' : 'Run Now'}
          </RunButton>
        </form>
      </DetailHeader>
      <DetailMeta>
        <MetaItem><MetaLabel>Schedule</MetaLabel> {job.schedule}</MetaItem>
        <MetaItem><MetaLabel>Last run</MetaLabel> {job.state === 'running' ? 'now' : formatRelative(job.lastRun)}</MetaItem>
        <MetaItem><MetaLabel>Duration</MetaLabel> {job.state === 'running' ? formatDuration(Date.now() - job.lastRun!) : formatDuration(job.lastDuration)}</MetaItem>
        <MetaItem><MetaLabel>Next run</MetaLabel> {formatRelative(job.nextRun)}</MetaItem>
      </DetailMeta>
      {job.lastError && (
        <OutputSection>
          <OutputLabel>Error</OutputLabel>
          <ErrorBlock>{job.lastError}</ErrorBlock>
        </OutputSection>
      )}
      {job.lastOutput ? (
        <OutputSection>
          <OutputLabel>Output</OutputLabel>
          <OutputBlock id="output">{job.lastOutput}</OutputBlock>
        </OutputSection>
      ) : job.state === 'running' ? (
        <OutputSection>
          <OutputLabel>Output</OutputLabel>
          <OutputBlock id="output" style={{ color: theme('colors-textMuted') }}>Waiting for output...</OutputBlock>
        </OutputSection>
      ) : job.lastRun && !job.lastError ? (
        <OutputSection>
          <EmptyState>No output</EmptyState>
        </OutputSection>
      ) : null}
      <script dangerouslySetInnerHTML={{ __html: `var o=document.getElementById('output');if(o)o.scrollTop=o.scrollHeight` }} />
    </Layout>
  )
})

app.get('/new', async c => {
  const appName = c.req.query('app') || ''

  return c.html(
    <Layout title="New Cron Job">
      <form method="post" action="/new">
        <Form>
          <input type="hidden" name="app" value={appName} />
          <FormGroup>
            <Label>Job Name</Label>
            <input name="name" placeholder="my-job" required pattern="[a-z0-9\-]+" title="lowercase letters, numbers, and hyphens only" style={inputStyles} />
          </FormGroup>
          <FormGroup>
            <Label>Run every</Label>
            <select name="schedule" style={inputStyles}>
              {SCHEDULES.map(s => (
                <option value={s} selected={s === 'day'}>{s}</option>
              ))}
            </select>
          </FormGroup>
          <ButtonRow>
            <CancelButton href="/">Cancel</CancelButton>
            <button type="submit" style={buttonStyles}>Create Job</button>
          </ButtonRow>
        </Form>
      </form>
    </Layout>
  )
})

app.post('/new', async c => {
  const body = await c.req.parseBody()
  const appName = body.app as string
  const name = body.name as string
  const schedule = body.schedule as string

  if (!appName || !name || !schedule) {
    return c.redirect('/new?error=missing-fields')
  }

  // Validate name (lowercase, numbers, hyphens)
  if (!/^[a-z0-9-]+$/.test(name)) {
    return c.redirect('/new?error=invalid-name')
  }

  const cronDir = join(APPS_DIR, appName, 'cron')
  const filePath = join(cronDir, `${name}.ts`)

  // Check if file already exists
  if (existsSync(filePath)) {
    return c.redirect('/new?error=already-exists')
  }

  // Create cron directory if needed
  if (!existsSync(cronDir)) {
    await mkdir(cronDir, { recursive: true })
  }

  // Write the cron file
  const content = `export const schedule = "${schedule}"

export default async function() {
  console.log("${appName}/${name} executed at", new Date().toISOString())
}
`

  await writeFile(filePath, content)
  console.log(`[cron] Created ${appName}:${name}`)

  // Trigger rediscovery
  const { jobs, invalid } = await discoverCronJobs()
  setJobs(jobs)
  setInvalidJobs(invalid)
  for (const job of jobs) {
    if (job.id === `${appName}:${name}`) {
      scheduleJob(job, broadcast)
      console.log(`[cron] Scheduled ${job.id}: ${job.schedule} (${job.cronExpr})`)
    }
  }

  return c.redirect('/')
})

app.post('/run/:app/:name', async c => {
  const id = `${c.req.param('app')}:${c.req.param('name')}`
  const job = getJob(id)

  if (!job) {
    return c.redirect('/?error=not-found')
  }

  // Fire-and-forget so the redirect happens immediately
  executeJob(job, broadcast)

  const returnTo = c.req.query('return')
  const appFilter = c.req.query('app')
  if (returnTo === 'detail') {
    return c.redirect(`/job/${job.app}/${job.name}${appFilter ? `?app=${appFilter}` : ''}`)
  }
  return c.redirect(appFilter ? `/?app=${appFilter}` : '/')
})

// Initialize
async function init() {
  const { jobs, invalid } = await discoverCronJobs()
  setJobs(jobs)
  setInvalidJobs(invalid)
  console.log(`[cron] Discovered ${jobs.length} jobs, ${invalid.length} invalid`)

  for (const job of jobs) {
    scheduleJob(job, broadcast)
    console.log(`[cron] Scheduled ${job.id}: ${job.schedule} (${job.cronExpr})`)
  }

  for (const job of invalid) {
    console.log(`[cron] Invalid ${job.id}: ${job.error}`)
  }
}

// Watch for cron file changes
let debounceTimer: Timer | null = null

async function rediscover() {
  const { jobs, invalid } = await discoverCronJobs()
  const existing = getAllJobs()

  // Stop removed jobs
  for (const old of existing) {
    if (!jobs.find(j => j.id === old.id)) {
      stopJob(old.id)
      console.log(`[cron] Removed ${old.id}`)
    }
  }

  // Add/update jobs
  for (const job of jobs) {
    const old = existing.find(j => j.id === job.id)
    if (!old || old.cronExpr !== job.cronExpr) {
      scheduleJob(job, broadcast)
      console.log(`[cron] Updated ${job.id}: ${job.schedule}`)
    } else {
      // Preserve runtime state
      job.state = old.state
      job.lastRun = old.lastRun
      job.lastDuration = old.lastDuration
      job.lastExitCode = old.lastExitCode
      job.lastError = old.lastError
      job.lastOutput = old.lastOutput
      job.nextRun = old.nextRun
    }
  }

  setJobs(jobs)
  setInvalidJobs(invalid)
}

watch(APPS_DIR, { recursive: true }, (_event, filename) => {
  if (!filename?.includes('/cron/') && !filename?.includes('\\cron\\')) return
  if (!filename.endsWith('.ts')) return

  if (debounceTimer) clearTimeout(debounceTimer)
  debounceTimer = setTimeout(rediscover, 100)
})

on(['app:reload', 'app:delete'], (event) => {
  console.log(`[cron] ${event.type} ${event.app}, rediscovering jobs...`)
  rediscover()
})

init()

export default app.defaults
