/**
 * JobDetail — per-Job home page (issue #351).
 *
 * Layout:
 *   ┌────────────────────────────────────────────────────────────────┐
 *   │ ← Back   <jobName>                              [STATUS CHIP]  │
 *   │ <jobId> · <appId> · <parentDisplayName> · last update HH:MM:SS │
 *   ├────────────────────────────────────────────────────────────────┤
 *   │                                                                 │
 *   │                full-bleed FlowPage canvas                       │
 *   │                                                                 │
 *   │                                                ┌──────────────┐ │
 *   │                                                │  LogPane     │ │
 *   │                                                │  (host job   │ │
 *   │                                                │   logs by    │ │
 *   │                                                │   default)   │ │
 *   │                                                └──────────────┘ │
 *   └────────────────────────────────────────────────────────────────┘
 *
 * Behavioural contract:
 *
 *   • The canvas opens with the page's host job auto-selected — its
 *     logs are visible in the LogPane on first paint.
 *   • Single-clicking another job swaps the LogPane's contents but
 *     the host's teal ring stays.
 *   • Double-clicking a leaf navigates to its own home; double-
 *     clicking a parent group toggles its fold state inline.
 *   • The LogPane is closeable; closing it restores the host as the
 *     selected job.
 *   • The canvas has no tabs and uses the entire viewport beneath the
 *     2-line header — replaces the previous Tabs.List / Tabs.Panel
 *     layout that wasted >80% of the canvas area.
 *
 * Per docs/INVIOLABLE-PRINCIPLES.md:
 *   #1 (waterfall) — full target shape ships in this PR. The previous
 *      tab strip is gone, not feature-flagged.
 *   #2 (no compromise) — no tab/accordion fallback; full-bleed canvas
 *      + floating pane is the only layout.
 *   #4 (never hardcode) — every label / id / route key is derived.
 */

import { useCallback, useMemo, useState } from 'react'
import { useParams, Link } from '@tanstack/react-router'
import { useWizardStore } from '@/entities/deployment/store'
import { PortalShell } from './PortalShell'
import { resolveApplications } from './applicationCatalog'
import { useDeploymentEvents } from './useDeploymentEvents'
import { deriveJobs, fmtTime, statusBadge } from './jobs'
import type { Job as DerivedJob, JobUiStatus, JobStep } from './jobs'
import { adaptDerivedJobsToFlat } from './jobsAdapter'
import { useLiveJobsBackfill, mergeJobs } from './useLiveJobsBackfill'
import { useJobDetail } from './useJobDetail'
import type { Job } from '@/lib/jobs.types'
import { LogPane } from '@/components/LogPane'
import type { LogLine, LogLevel } from '@/components/ExecutionLogs'
import { FlowPage } from './FlowPage'

interface JobDetailProps {
  /** Test seam — disables the live SSE EventSource attach. */
  disableStream?: boolean
  /** Test seam — disables the live-jobs backfill polling. */
  disableJobsBackfill?: boolean
}

export function JobDetail({
  disableStream = false,
  disableJobsBackfill = false,
}: JobDetailProps = {}) {
  const params = useParams({
    from: '/provision/$deploymentId/jobs/$jobId' as never,
  }) as { deploymentId: string; jobId: string }
  const { deploymentId, jobId } = params
  const store = useWizardStore()

  const applications = useMemo(
    () => resolveApplications(store.selectedComponents),
    [store.selectedComponents],
  )
  const applicationIds = useMemo(() => applications.map((a) => a.id), [applications])

  const { state, snapshot, streamStatus } = useDeploymentEvents({
    deploymentId,
    applicationIds,
    disableStream,
  })

  const sovereignFQDN = snapshot?.sovereignFQDN ?? snapshot?.result?.sovereignFQDN ?? null

  const derivedJobs = useMemo(() => deriveJobs(state, applications), [state, applications])
  const derivedJobsById = useMemo<Record<string, DerivedJob>>(() => {
    const out: Record<string, DerivedJob> = {}
    for (const j of derivedJobs) out[j.id] = j
    return out
  }, [derivedJobs])
  const reducerJobs = useMemo(() => adaptDerivedJobsToFlat(derivedJobs), [derivedJobs])
  const inFlight = streamStatus !== 'completed' && streamStatus !== 'failed'
  const { liveJobs } = useLiveJobsBackfill({
    deploymentId,
    enabled: !disableJobsBackfill,
    disablePolling: disableJobsBackfill || !inFlight,
  })
  const jobs = useMemo(
    () => mergeJobs(reducerJobs, liveJobs),
    [reducerJobs, liveJobs],
  )
  const jobsById = useMemo<Record<string, Job>>(() => {
    const out: Record<string, Job> = {}
    for (const j of jobs) out[j.id] = j
    return out
  }, [jobs])
  const job = jobsById[jobId]

  // Host job = the page owner. The canvas paints a persistent teal
  // ring on this id and the LogPane defaults to the host's logs.
  // The selected-for-logs id can change as the operator clicks
  // around; the host stays put.
  const [selectedJobId, setSelectedJobId] = useState<string>(jobId)
  const selectedJob: Job | null = jobsById[selectedJobId] ?? job ?? null
  // Pane visibility — open by default. The X / Esc dismisses it;
  // any explicit canvas-bubble click reopens it.
  const [paneOpen, setPaneOpen] = useState<boolean>(true)

  // Resolve the REAL execution id by polling the per-job detail
  // endpoint. The selected job (NOT the host) drives this so the
  // pane updates when the operator clicks around.
  const detail = useJobDetail({
    deploymentId,
    jobId: selectedJob?.id ?? jobId,
    disablePolling: disableJobsBackfill || !inFlight,
    enabled: !disableJobsBackfill,
  })
  const executionId = detail.latestExecutionId
  const detailJobStatus = detail.job?.status

  // Bug #481 — derived-job fallback log lines. When `useJobDetail` has no
  // Bridge-allocated execution (Phase-0 tofu jobs, cluster-bootstrap, or
  // any per-bp-* job whose Helm watch has not yet emitted an execution
  // row), the LogPane would render the "No execution recorded yet"
  // empty state — even though we already have the SSE event log buffered
  // in derivedJobs[selectedJobId].steps. Map those steps to LogLines so
  // the operator sees the captured events.
  const fallbackLines = useMemo<LogLine[]>(() => {
    if (executionId) return [] // Real execution wins — don't double-render.
    const id = selectedJob?.id ?? jobId
    const dj = derivedJobsById[id]
    if (!dj) return []
    return stepsToLogLines(dj.steps)
  }, [executionId, selectedJob, jobId, derivedJobsById])

  const onCanvasJobSelect = useCallback(
    (id: string | null) => {
      // Empty / null — operator clicked the canvas background; restore
      // the host as the selected job so the LogPane never goes
      // contextless. (Background click does NOT dismiss the pane —
      // only the explicit X button or Esc does that.)
      setSelectedJobId(id ?? jobId)
      // Any explicit selection re-opens the pane if the operator had
      // dismissed it earlier.
      if (id) setPaneOpen(true)
    },
    [jobId],
  )

  if (!job) {
    return (
      <PortalShell
        deploymentId={deploymentId}
        sovereignFQDN={sovereignFQDN}
        pageTitle="Job not found"
      >
        <div className="mx-auto max-w-3xl py-8" data-testid="job-detail-not-found">
          <Link
            to="/provision/$deploymentId/jobs"
            params={{ deploymentId }}
            className="text-xs text-[var(--color-text-dim)] hover:text-[var(--color-text)] no-underline"
            data-testid="job-detail-back"
          >
            ← Back to jobs
          </Link>
          <div className="mt-6 rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] p-8 text-center">
            <h1 className="text-lg font-semibold text-[var(--color-text-strong)]">Job not found</h1>
            <p className="mt-2 text-sm text-[var(--color-text-dim)]">
              <code className="font-mono">{jobId}</code> is not part of this deployment.
            </p>
          </div>
        </div>
      </PortalShell>
    )
  }

  const badge = statusBadge(job.status as JobUiStatus)
  const lastUpdate = job.finishedAt ?? job.startedAt
  const parent = job.parentId ? jobsById[job.parentId] ?? null : null
  const parentLabel = parent ? parent.displayName ?? parent.jobName : null
  const titleLabel = job.displayName ?? job.jobName

  // Subtitle pieces — composed into a single line so the chrome stays
  // at 2 lines (header title + meta). Empty pieces drop out.
  const subtitlePieces: string[] = [job.id]
  if (job.appId && job.appId !== 'infrastructure' && job.appId !== 'cluster-bootstrap' && job.type !== 'group') {
    subtitlePieces.push(job.appId)
  }
  if (parentLabel) subtitlePieces.push(parentLabel)
  const lastUpdateLabel = fmtTime(lastUpdate)
  if (lastUpdateLabel) subtitlePieces.push(`last update ${lastUpdateLabel}`)

  // LogPane title + status — driven by the SELECTED job (operator
  // clicked something), defaulting to the host job.
  const logPaneJob = selectedJob ?? job
  const logPaneStatus = (detailJobStatus as JobUiStatus | undefined) ?? logPaneJob.status

  return (
    <PortalShell
      deploymentId={deploymentId}
      sovereignFQDN={sovereignFQDN}
      pageTitle={titleLabel}
    >
      <div
        className={`job-detail-page${paneOpen ? '' : ' is-pane-closed'}`}
        data-testid={`job-detail-${jobId}`}
      >
        <style>{JOB_DETAIL_CSS}</style>

        {/* Two-line compact header with status chip top-right. */}
        <header className="job-detail-header" data-testid="job-detail-header">
          <div className="job-detail-header-titles">
            <div className="job-detail-header-line1">
              <Link
                to="/provision/$deploymentId/jobs"
                params={{ deploymentId }}
                className="job-detail-back"
                data-testid="job-detail-back"
              >
                ← Back to jobs
              </Link>
              <h1 className="job-detail-title" data-testid="job-detail-title" title={titleLabel}>
                {titleLabel}
              </h1>
            </div>
            <div className="job-detail-header-line2" data-testid="job-detail-meta">
              {subtitlePieces.map((p, idx) => (
                <span key={idx} className="job-detail-meta-piece">
                  {idx > 0 ? <span className="job-detail-meta-sep" aria-hidden> · </span> : null}
                  {p}
                </span>
              ))}
            </div>
          </div>
          <span
            className={`job-detail-status-chip ${badge.classes}`}
            data-testid="job-detail-status"
          >
            {badge.text}
          </span>
        </header>

        {/* Full-bleed canvas. The embedded FlowPage owns its own
            chrome-less surface; it streams hostJobId so the canvas
            paints the teal home ring. The `paneOpen` flag widens the
            canvas back out to the full viewport when the operator
            dismisses the LogPane. */}
        <div
          className={`job-detail-canvas${paneOpen ? '' : ' is-pane-closed'}`}
          data-testid="job-detail-canvas"
          data-pane-open={paneOpen ? 'true' : 'false'}
        >
          <FlowPage
            disableStream={disableStream}
            disableJobsBackfill={disableJobsBackfill || disableStream}
            embedded
            deploymentIdOverride={deploymentId}
            hostJobId={job.id}
            onOpenJobChange={onCanvasJobSelect}
          />
        </div>

        {/* When the pane is closed, surface a small re-open button so
            the operator can recover the log surface without having to
            click a bubble. Stays out of the way (top-right of the
            canvas area). */}
        {!paneOpen ? (
          <button
            type="button"
            className="job-detail-reopen-pane"
            data-testid="job-detail-reopen-pane"
            aria-label="Show log pane"
            title="Show log pane"
            onClick={() => setPaneOpen(true)}
          >
            <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden>
              <path d="M2 3 L12 3 M2 7 L12 7 M2 11 L8 11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
            </svg>
            Logs
          </button>
        ) : null}
      </div>

      {/* Floating log pane — open by default on the host's logs.
          The X button + Esc dismiss it; the canvas widens to fill
          the full viewport. Single-click on any bubble re-opens it. */}
      {paneOpen ? (
        <CanvasLogBridge
          executionId={executionId}
          fallbackLines={fallbackLines}
          jobTitle={logPaneJob.displayName ?? logPaneJob.jobName}
          jobStatus={logPaneStatus}
          onClose={() => {
            setPaneOpen(false)
            setSelectedJobId(jobId)
          }}
        />
      ) : null}
    </PortalShell>
  )
}

/* ──────────────────────────────────────────────────────────────────
 * CanvasLogBridge — bridges the floating LogPane into JobDetail.
 *
 * The pane is its own positioned element, mounted at the page level
 * so it overlays the canvas without changing the canvas layout. Kept
 * as a tiny presentational shim so the JobDetail body stays readable.
 * ────────────────────────────────────────────────────────────────── */

interface CanvasLogBridgeProps {
  executionId: string | null | undefined
  /** Bug #481 — derived-job fallback lines (Phase-0, cluster-bootstrap). */
  fallbackLines: readonly LogLine[]
  jobTitle: string
  jobStatus: string
  onClose: () => void
}

function CanvasLogBridge({
  executionId,
  fallbackLines,
  jobTitle,
  jobStatus,
  onClose,
}: CanvasLogBridgeProps) {
  const tone: 'pending' | 'running' | 'succeeded' | 'failed' =
    jobStatus === 'running' || jobStatus === 'succeeded' || jobStatus === 'failed'
      ? jobStatus
      : 'pending'
  return (
    <LogPane
      executionId={executionId ?? null}
      fallbackLines={fallbackLines}
      jobTitle={jobTitle}
      statusLabel={jobStatus}
      statusTone={tone}
      onClose={onClose}
    />
  )
}

/* ──────────────────────────────────────────────────────────────────
 * stepsToLogLines — DerivedJob.step[] → LogLine[] adapter (Bug #481)
 *
 * The reducer state owns the live SSE event log for derived jobs
 * (Phase-0 tofu, cluster-bootstrap, per-bp-* installs whose Helm
 * watcher hasn't allocated an Execution row yet). Each step maps 1:1
 * to a LogLine — we just need to pick a sensible LogLevel from the
 * step's status + name and number the lines from 1.
 * ────────────────────────────────────────────────────────────────── */

function stepsToLogLines(steps: readonly JobStep[]): LogLine[] {
  return steps.map((s, i): LogLine => {
    let level: LogLevel = 'INFO'
    if (s.status === 'failed') level = 'ERROR'
    // Heuristic: name strings starting with a `-` / `+` / `Initializing`
    // are tofu CLI noise — surface them as DEBUG so a level-filter on
    // INFO-only doesn't drown the operator. The reducer doesn't carry
    // a level field for derived steps, so this is the cleanest place
    // to derive one.
    else if (
      s.name.startsWith('+ ') ||
      s.name.startsWith('- ') ||
      s.name.startsWith('# ')
    ) {
      level = 'DEBUG'
    }
    return {
      lineNumber: i + 1,
      timestamp: s.startedAt ?? '',
      level,
      message: s.name,
    }
  })
}

const JOB_DETAIL_CSS = `
.job-detail-page {
  position: relative;
  display: flex;
  flex-direction: column;
  height: calc(100vh - 56px);
  width: 100%;
  padding: 0.75rem 1rem 0;
  box-sizing: border-box;
  /* Reserve space for the floating LogPane on the right so the canvas
     centroid lands inside the visible area instead of being hidden
     behind the pane. The LogPane width is also bound to this var via
     --log-pane-width, set on the root below. When the operator
     dismisses the pane (paneOpen=false → .is-pane-closed) the canvas
     reclaims the reserved space. */
  padding-right: calc(var(--log-pane-width, 30vw) + 1rem);
  min-width: 0;
  transition: padding-right 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
.job-detail-page.is-pane-closed {
  padding-right: 1rem;
}
.job-detail-reopen-pane {
  position: absolute;
  top: 12px;
  right: 12px;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.32rem 0.7rem;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-dim);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  z-index: 50;
  transition: color 0.12s ease, background-color 0.12s ease, border-color 0.12s ease;
}
.job-detail-reopen-pane:hover {
  color: var(--color-text-strong);
  border-color: var(--color-text-dim);
  background: rgba(148, 163, 184, 0.08);
}

.job-detail-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 0.45rem;
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}
.job-detail-header-titles {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1 1 auto;
  gap: 0.05rem;
}
.job-detail-header-line1 {
  display: flex;
  align-items: baseline;
  gap: 0.7rem;
  min-width: 0;
}
.job-detail-back {
  font-size: 0.72rem;
  color: var(--color-text-dim);
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
  transition: color 0.12s ease;
}
.job-detail-back:hover { color: var(--color-text-strong); }
.job-detail-title {
  margin: 0;
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--color-text-strong);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1 1 auto;
  min-width: 0;
}
.job-detail-header-line2 {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.72rem;
  color: var(--color-text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.job-detail-meta-sep {
  margin: 0 0.4rem;
  opacity: 0.6;
}
.job-detail-status-chip {
  flex-shrink: 0;
  border-radius: 999px;
  padding: 0.18rem 0.7rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.job-detail-canvas {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  margin-top: 0.6rem;
}
.job-detail-canvas > * {
  flex: 1 1 auto;
  min-height: 0;
  height: 100%;
}
@media (max-width: 1100px) {
  .job-detail-page {
    padding-right: 1rem;
  }
}
`
