import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import { MasterDetail } from '@/components/templates'
import {
  Alert,
  Badge,
  Button,
  Card,
  ConfirmDialog,
  EmptyState,
  Field,
  Input,
  SegmentedControl,
  Toggle,
  useToast,
  type Column,
} from '@/components/ui'
import { serverMessage } from '@/lib/api'
import { formatRelative } from '@/lib/format'
import { usePlatformBrand } from '@/lib/brand'
import {
  useCrudPanel,
  useCreatePrinter,
  usePrintAgentSetup,
  useRotatePrintAgentKey,
  useDeletePrinter,
  usePrinters,
  useUpdatePrinter,
  type StorePrinter,
} from '../hooks'

/**
 * Printers — the devices this branch prints receipts to.
 *
 * Two worlds, and this page's job is to make the difference plain.
 * `browserPopupPrint` is the till printing itself through the browser's own
 * dialog, which is what every store has out of the box. `directPrint` hands the
 * receipt to the DESKTOP PRINT AGENT — a separate app that polls this server and
 * prints straight to the device with no dialog at all, which is the only way a
 * web POS reaches a thermal printer without the cashier confirming a box on
 * every single sale.
 *
 * The field names are the agent's, not ours (`printing_choice`, `share_name`,
 * `type`): it is a separate repo that has already shipped, and renaming its wire
 * contract would break every install running it.
 */
type Draft = {
  name: string
  printing_choice: 'directPrint' | 'browserPopupPrint'
  /** The roll THIS printer takes — its own field, not a store-wide default. */
  print_format: 'thermal56mm' | 'thermal80mm' | 'thermal122mm'
  is_active: boolean
  is_default: boolean
}

const empty: Draft = {
  name: '',
  printing_choice: 'browserPopupPrint',
  print_format: 'thermal80mm',
  is_active: true,
  is_default: false,
}

/** The three rolls this shop's hardware actually ships in — see PrinterRequest. */
function paperLabel(format: Draft['print_format'] | null | undefined, t: (s: string) => string): string {
  if (format === 'thermal56mm') return t('58 mm')
  if (format === 'thermal122mm') return t('122 mm')
  return t('80 mm')
}

export default function PrintersPage() {
  const { t } = useTranslation()
  const platform = usePlatformBrand()
  const toast = useToast()
  const navigate = useNavigate()

  const { data, isLoading } = usePrinters()
  const { data: agent } = usePrintAgentSetup()
  const rotate = useRotatePrintAgentKey()
  const [confirmRotate, setConfirmRotate] = useState(false)
  const create = useCreatePrinter()
  const update = useUpdatePrinter()
  const del = useDeletePrinter()

  const panel = useCrudPanel<StorePrinter, Draft>(empty, (p) => ({
    name: p.name,
    printing_choice: p.printing_choice,
    print_format: p.print_format ?? 'thermal80mm',
    is_active: p.is_active,
    is_default: p.is_default,
  }))
  const [confirmDelete, setConfirmDelete] = useState(false)

  // A suggested starting roll for a brand-new printer — the branch's own
  // existing default, the same source the backend falls back to if this
  // isn't touched (PrinterController::formatForRoll()). Purely a UI nicety:
  // the picker would otherwise show a blank before the first save.
  const branchDefaultFormat = (data ?? []).find((p) => p.is_default)?.print_format ?? 'thermal80mm'

  const d = panel.draft

  const save = () => {
    if (!d.name.trim()) {
      toast.danger(t('Name required'), t('Give the printer a name you will recognise at the till.'))
      return
    }

    const input = {
      name: d.name.trim(),
      printing_choice: d.printing_choice,
      print_format: d.print_format,
      is_active: d.is_active,
      is_default: d.is_default,
    }

    const onError = (e: unknown) =>
      toast.danger(t('Could not save'), serverMessage(e, t('Please try again.')))
    const onSuccess = () => {
      toast.success(t('Printer saved'), t('{{name}} was saved', { name: input.name }))
      panel.close()
    }

    if (panel.isNew) create.mutate(input, { onSuccess, onError })
    else update.mutate({ ...input, id: panel.editing!.id }, { onSuccess, onError })
  }

  const columns: Column<StorePrinter>[] = [
    {
      key: 'name',
      header: t('Printer'),
      cell: (p) => (
        <div className="flex items-center gap-2">
          <b className="text-content">{p.name}</b>
          {p.is_default && <Badge tone="accent">{t('Default')}</Badge>}
        </div>
      ),
    },
    {
      key: 'how',
      header: t('Prints via'),
      cell: (p) =>
        p.printing_choice === 'directPrint' ? (
          <Badge tone="accent">{t('Print agent')}</Badge>
        ) : (
          <Badge tone="neutral">{t('Browser dialog')}</Badge>
        ),
    },
    {
      key: 'paper',
      header: t('Paper'),
      // Was "Sends to", showing share_name — a column the reference codebase
      // never fills and the agent never reads, so it rendered a dash on every row.
      // Shown for every printer regardless of "Prints via": print_format sizes
      // the printed page for a Browser printer exactly as much as a Print
      // agent one (usePrintHardware.ts) — it used to read as direct-print-only
      // here because nothing consulted it for Browser printers either.
      cell: (p) => <span className="text-content-2">{paperLabel(p.print_format, t)}</span>,
    },
    {
      key: 'active',
      header: t('Status'),
      cell: (p) =>
        p.is_active ? <Badge tone="accent">{t('Active')}</Badge> : <Badge tone="neutral">{t('Off')}</Badge>,
    },
  ]

  return (
    <>
      {/*
        The agent's connection details, on the page where someone setting it up
        is already standing. Without this the key — which every branch has had
        since the rewrite — was reachable only with a database query, so the one
        thing the agent asks for could not be answered from the app at all.
      */}
      {agent && (
        <Card
          title={t('Connect the print agent')}
          subtitle={t('Enter these on the computer the printer is plugged into — this is the key the agent asks for, not an API key')}
          className="mb-4"
        >
          {/* A correct key and a correct address can both be true while the
              agent itself sits closed — the exact gap that let a sale report
              "printed" while nothing was left to collect it (see
              PrintJobService::agentOnline()). This is that same check, shown
              rather than discovered the next time a receipt silently
              doesn't print. */}
          <div className="mb-3 flex items-center gap-2 text-sm">
            {agent.agent_online ? (
              <Badge tone="accent">{t('Agent connected')}</Badge>
            ) : (
              <Badge tone="neutral">{t('Agent not connected')}</Badge>
            )}
            <span className="text-content-3">
              {agent.agent_last_seen_at
                ? t('Last seen {{when}}', { when: formatRelative(agent.agent_last_seen_at) })
                : t('Never connected')}
            </span>
          </div>

          <div className="grid gap-3 sm:grid-cols-2">
            <CopyField label={t('Server address')} value={agent.url} />
            <CopyField label={t('Branch key')} value={agent.key} secret />
          </div>
          <p className="mt-3 text-sm text-content-3">
            {t('The key is this branch’s own — {{branch}}. A till at another branch needs that branch’s key, or its receipts will print here.', { branch: agent.branch_name })}
          </p>

          <div className="mt-3 flex flex-wrap gap-2">
            <Button variant="ghost" size="sm" onClick={() => setConfirmRotate(true)}>
              {t('Issue a new key')}
            </Button>
            <Button variant="ghost" size="sm" icon="download" onClick={() => navigate('/settings/downloads')}>
              {t('Download the agent')}
            </Button>
          </div>

          {/* The one address that looks right and cannot work from another
              machine. Said as a condition rather than an error: on a single
              computer running both, it is correct. */}
          {agent.same_machine_only && (
            <Alert
              tone="warning"
              className="mt-3"
              title={t('This address only works on this computer')}
              message={t('It points at the machine {{name}} is running on. If the printer is plugged into a different computer, that agent needs this server’s network address or domain instead.', { name: platform.name })}
            />
          )}
        </Card>
      )}

      <MasterDetail<StorePrinter>
        writePermission="Manage Hardware"
        title={t('Printers')}
        microcopy={t('Where receipts rung at this branch are sent. A direct printer prints on its own; anything else goes through your browser’s print dialog.')}
        addLabel={t('Add printer')}
        onAdd={() => panel.openNew({ print_format: branchDefaultFormat })}
        loading={isLoading}
        columns={columns}
        rows={data ?? []}
        rowKey={(p) => String(p.id)}
        onRowClick={panel.openEdit}
        isRowActive={(p) => p.id === panel.editing?.id}
        emptyState={
          <EmptyState
            icon="printer"
            title={t('No printers yet')}
            description={t('Add the printer at this counter so receipts know where to go.')}
          />
        }
        detailOpen={panel.open}
        onCloseDetail={panel.close}
        detailTitle={panel.isNew ? t('New printer') : panel.editing?.name}
        detailSubtitle={t('printer')}
        dirty={panel.dirty}
        saving={create.isPending || update.isPending}
        onSave={save}
        onDiscard={panel.close}
        onDelete={panel.editing ? () => setConfirmDelete(true) : undefined}
        detail={
          <div className="grid gap-4">
            {/* Says plainly what a shop must have for direct printing to work.
                Without it the option reads like a switch, and a shopkeeper would
                turn it on and be left wondering why nothing prints. */}
            <Alert
              tone="info"
              title={t('Direct printing needs the print agent')}
              message={t('It runs on the computer the printer is plugged into. In the agent, map this printer’s NAME to the physical printer — that name is how the agent recognises it.')}
              actionLabel={t('Get the agent')}
              onAction={() => navigate('/settings/downloads')}
            />

            {/* The name is not a label — it is the KEY the agent maps on. Its
                log says "Empty mapping for API printer: <name>" when it has not
                been assigned a local printer, so the name here and the name in
                the agent must match exactly. */}
            <Input
              label={t('Name')}
              required
              value={d.name}
              onChange={(e) => panel.patch({ name: e.target.value })}
              placeholder={t('e.g. Counter thermal')}
              help={t('Use this exact name when you map it to a printer in the agent.')}
            />

            <Field label={t('Prints via')}>
              <SegmentedControl<'browserPopupPrint' | 'directPrint'>
                value={d.printing_choice}
                onChange={(x) => panel.patch({ printing_choice: x })}
                options={[
                  { value: 'browserPopupPrint', label: t('Browser') },
                  { value: 'directPrint', label: t('Print agent') },
                ]}
              />
            </Field>

            {/*
              The reference codebase's Edit Printer form has no connection or
              address field at all — its `share_name` and `type` columns are
              always null — and its agent prints regardless, because the agent
              maps this row's NAME to a local printer in its own configuration.
              Both fields were an invention and are gone; Paper is the one
              field that form really has.

              Shown regardless of "Prints via": this used to be gated to
              directPrint only, which matched a real bug rather than fixing
              it — usePrintHardware.ts was ALSO only reading a printer's own
              print_format when it was a directPrint printer, so a Browser
              printer's roll size genuinely had no effect anywhere. Now that
              the hook resolves paper width independently of printing_choice,
              hiding this field for Browser printers would be the same bug
              in reverse — a setting with a real effect the form won't let
              you see or change.
            */}
            <Field label={t('Paper')}>
              <SegmentedControl<'thermal56mm' | 'thermal80mm' | 'thermal122mm'>
                value={d.print_format}
                onChange={(x) => panel.patch({ print_format: x })}
                options={[
                  { value: 'thermal56mm', label: t('58 mm') },
                  { value: 'thermal80mm', label: t('80 mm') },
                  { value: 'thermal122mm', label: t('122 mm') },
                ]}
              />
              {/* Genuinely per-printer now — the reference codebase's own
                  Edit Printer form sets this on the printer row too. A branch with a
                  58mm counter printer and an 80mm kitchen printer needs
                  both to hold their own value; this used to be overwritten
                  from Settings → Printer & drawer on every save, so a
                  second differently-sized printer could never actually be
                  set up correctly. */}
              <p className="mt-1 text-sm text-content-3">
                {t('The roll this printer is loaded with. A new printer starts at this branch’s existing default — change it here without affecting other printers.')}
              </p>
            </Field>

            <Toggle
              checked={d.is_default}
              onChange={(x) => panel.patch({ is_default: x })}
              label={t('Use this one for receipts at this branch')}
            />
            <Toggle
              checked={d.is_active}
              onChange={(x) => panel.patch({ is_active: x })}
              label={t('Active')}
            />
          </div>
        }
      />

      <ConfirmDialog
        open={confirmRotate}
        onClose={() => setConfirmRotate(false)}
        title={t('Issue a new key for this branch?')}
        consequence={t('The current key stops working straight away. Every print agent at this branch will stop printing until the new key is entered on it.')}
        confirmLabel={t('Issue new key')}
        onConfirm={() =>
          rotate.mutate(undefined, {
            onSuccess: () => {
              toast.success(t('New key issued'), t('Enter it on each print agent at this branch.'))
              setConfirmRotate(false)
            },
            onError: (e) => {
              setConfirmRotate(false)
              toast.danger(t('Could not issue a key'), serverMessage(e, t('Please try again.')))
            },
          })
        }
      />

      <ConfirmDialog
        open={confirmDelete}
        onClose={() => setConfirmDelete(false)}
        title={t('Delete this printer?')}
        consequence={t('Receipts will print through the browser instead, until another printer is made the default for this branch.')}
        confirmLabel={t('Delete')}
        onConfirm={() =>
          del.mutate(panel.editing!.id, {
            onSuccess: () => {
              toast.success(t('Printer deleted'))
              setConfirmDelete(false)
              panel.close()
            },
            onError: (e) => {
              setConfirmDelete(false)
              toast.danger(t('Could not delete'), serverMessage(e, t('Please try again.')))
            },
          })
        }
      />
    </>
  )
}

/**
 * A value the shopkeeper has to get into another application intact.
 *
 * Read-only with a Copy button rather than a plain input: these are typed into
 * a different machine, and a key transcribed with one character wrong fails in
 * exactly the same way as a key that is right but unreachable — the agent polls
 * and nothing ever prints.
 */
function CopyField({ label, value, secret }: { label: string; value: string; secret?: boolean }) {
  const { t } = useTranslation()
  const toast = useToast()
  const [shown, setShown] = useState(!secret)

  return (
    <Field label={label}>
      <div className="flex items-center gap-2">
        {/* `wrap={false}`: Input otherwise renders its own `.field` wrapper, and
            Ledger gives that a hard `min-width: 210px` — inside this flex row,
            in a two-column grid, that overflows the card rather than shrinking.
            The wrapper is the flex child, so styling the control cannot fix it. */}
        <Input
          readOnly
          mono
          wrap={false}
          value={shown ? value : '•'.repeat(Math.min(value.length, 20))}
          onFocus={(e) => e.currentTarget.select()}
          className="min-w-0 flex-1"
        />
        {secret && (
          <Button variant="ghost" size="sm" onClick={() => setShown((v) => !v)}>
            {shown ? t('Hide') : t('Show')}
          </Button>
        )}
        <Button
          variant="secondary"
          size="sm"
          icon="copy"
          onClick={async () => {
            await navigator.clipboard.writeText(value)
            toast.success(t('Copied'))
          }}
        >
          {t('Copy')}
        </Button>
      </div>
    </Field>
  )
}
