import type { UpstreamConfig } from './upstream';
import { callUpstream, resolveCompanyHash } from './upstream';

// The Officer-owned route surface. Every path here is deliberate: this is an allow-list, not a passthrough.
//
// InvoiceShelf's API is already REST, so most of these are shaped proxies rather than translations — the
// value the sidecar adds is (a) holding the credential, (b) pinning the company, (c) repairing the PDF
// responses, and (d) refusing to expose the administrative half of the API at all.
//
// DELIBERATELY NOT EXPOSED: backups, disks, modules, update/*, installation/*, mail config and test-send,
// settings writes, and transfer/ownership. Those either mutate the deployment or can lock the owner out of
// it, and none of them belong behind a dashboard proxy. Adding one should be a decision, not an accident.

type Capabilities = { write: boolean; bulkDelete: boolean };

const RESOURCES: Record<string, Capabilities> = {
  invoices: { write: true, bulkDelete: true },
  estimates: { write: true, bulkDelete: true },
  'recurring-invoices': { write: true, bulkDelete: true },
  payments: { write: true, bulkDelete: true },
  expenses: { write: true, bulkDelete: true },
  customers: { write: true, bulkDelete: true },
  items: { write: true, bulkDelete: true },
  units: { write: true, bulkDelete: false },
  categories: { write: true, bulkDelete: false },
  'tax-types': { write: true, bulkDelete: false },
  'payment-methods': { write: true, bulkDelete: false },
  notes: { write: true, bulkDelete: false },
  'custom-fields': { write: true, bulkDelete: false },
  currencies: { write: false, bulkDelete: false },
  countries: { write: false, bulkDelete: false },
  users: { write: false, bulkDelete: false },
  roles: { write: false, bulkDelete: false },
};

// Non-REST sub-actions, per resource. Verified against `artisan route:list` on the LIVE 2.4.2 instance
// rather than the 3.0.0-alpha checkout in _references — they differ. Notably 2.4.2 has
// estimates/{id}/convert-to-invoice but NO invoices/{id}/convert-to-estimate, which 3.0 added.
const ACTIONS: Record<string, readonly string[]> = {
  invoices: ['status', 'clone', 'send'],
  estimates: ['status', 'clone', 'send', 'convert-to-invoice'],
  payments: ['send'],
  expenses: ['duplicate'],
};

// Resources whose PDF lives on a web route keyed by `unique_hash`.
const PDF_RESOURCES = new Set(['invoices', 'estimates', 'payments']);

const REPORTS: Record<string, string> = {
  'sales-customers': 'reports/sales/customers',
  'sales-items': 'reports/sales/items',
  'tax-summary': 'reports/tax-summary',
  'profit-loss': 'reports/profit-loss',
  expenses: 'reports/expenses',
};

const notFound = () => Response.json({ error: 'not found' }, { status: 404 });
const badRequest = (error: string) => Response.json({ error }, { status: 400 });

/** Pass an upstream JSON response through untouched — including Laravel's 422 `errors{}` envelope. */
async function passthrough(res: Response): Promise<Response> {
  const body = await res.arrayBuffer();
  const headers = new Headers();
  const type = res.headers.get('content-type');
  if (type) headers.set('Content-Type', type);
  return new Response(body, { status: res.status, headers });
}

/**
 * InvoiceShelf 2.4.2 prefixes the document PDFs with a literal serialised HTTP response — the body of
 * `/invoices/pdf/{hash}` starts `HTTP/1.0 200 OK\r\nCache-Control: …\r\n\r\n%PDF-1.7`, 201 bytes of it,
 * inside a response whose own Content-Type is already application/pdf. Every PDF reader rejects that.
 *
 * So: find the `%PDF-` magic and slice from there. Verified on this instance — the document routes carry the
 * prefix, the report routes do not, and slicing is a no-op when the magic is already at offset 0.
 */
function repairPdf(bytes: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer> | null {
  const magic = [0x25, 0x50, 0x44, 0x46, 0x2d]; // %PDF-
  const limit = Math.min(bytes.length - magic.length, 4096);
  for (let i = 0; i <= limit; i++) {
    let hit = true;
    for (let j = 0; j < magic.length; j++) {
      if (bytes[i + j] !== magic[j]) {
        hit = false;
        break;
      }
    }
    if (hit) return i === 0 ? bytes : bytes.subarray(i);
  }
  return null;
}

type PdfParams = { cfg: UpstreamConfig; path: string; query: string; filename: string };

async function servePdf({ cfg, path, query, filename }: PdfParams): Promise<Response> {
  const res = await callUpstream(cfg, { path, query, accept: 'application/pdf' });
  if (!res.ok) return passthrough(res);

  const repaired = repairPdf(new Uint8Array(await res.arrayBuffer()));
  if (!repaired) return Response.json({ error: 'upstream did not return a PDF' }, { status: 502 });

  return new Response(new Blob([repaired]), {
    status: 200,
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `inline; filename="${filename}"`,
      'Cache-Control': 'no-store',
    },
  });
}

/** Look up a document's `unique_hash`, which its PDF web route is keyed by. */
async function fetchUniqueHash(cfg: UpstreamConfig, resource: string, id: string): Promise<string | null> {
  const res = await callUpstream(cfg, { path: `/api/v1/${resource}/${id}` });
  if (!res.ok) return null;
  const payload = (await res.json()) as { data?: { unique_hash?: string } };
  return payload.data?.unique_hash ?? null;
}

async function readJson(req: Request): Promise<unknown> {
  try {
    return await req.json();
  } catch {
    return null;
  }
}

type Bootstrap = {
  current_user?: unknown;
  current_company?: unknown;
  current_company_currency?: unknown;
  current_company_settings?: unknown;
};

/**
 * Everything a dashboard needs on first paint, in one round trip: who we are, which company we are pinned to,
 * how to format its money, and the totals.
 *
 * The currency matters more than it looks. Every amount in this API is an integer in minor units, and
 * InvoiceShelf's own frontend just divides by 100 — but `current_company_currency` carries the real
 * precision, separators and `swap_currency_symbol` (this instance is EUR: "1.234,56 €"). A client that
 * formats from these fields is right for currencies the upstream's own UI gets wrong.
 */
async function summary(cfg: UpstreamConfig): Promise<Response> {
  const [bootRes, dashRes] = await Promise.all([
    callUpstream(cfg, { path: '/api/v1/bootstrap' }),
    callUpstream(cfg, { path: '/api/v1/dashboard' }),
  ]);

  if (!dashRes.ok) return passthrough(dashRes);

  const boot = bootRes.ok ? ((await bootRes.json()) as Bootstrap) : null;
  const dashboard = await dashRes.json();

  return Response.json({
    me: boot?.current_user ?? null,
    company: boot?.current_company ?? null,
    currency: boot?.current_company_currency ?? null,
    settings: boot?.current_company_settings ?? null,
    dashboard,
  });
}

/**
 * The reference-data a form needs, fetched in parallel. All of these are small and change rarely, so one
 * round trip beats seven — and `limit=all` keeps customers and items out of pagination.
 */
async function lookups(cfg: UpstreamConfig): Promise<Response> {
  const wanted = [
    ['customers', '/api/v1/customers?limit=all'],
    ['items', '/api/v1/items?limit=all'],
    ['units', '/api/v1/units?limit=all'],
    ['taxTypes', '/api/v1/tax-types?limit=all'],
    ['categories', '/api/v1/categories?limit=all'],
    ['paymentMethods', '/api/v1/payment-methods?limit=all'],
    ['currencies', '/api/v1/currencies'],
  ] as const;

  const entries = await Promise.all(
    wanted.map(async ([key, path]) => {
      const res = await callUpstream(cfg, { path });
      if (!res.ok) return [key, []] as const;
      const payload = (await res.json()) as { data?: unknown };
      return [key, payload.data ?? []] as const;
    }),
  );

  return Response.json(Object.fromEntries(entries));
}

export async function handleOfficerRoute(cfg: UpstreamConfig, req: Request, url: URL): Promise<Response | null> {
  const segments = url.pathname
    .replace(/^\/_officer\/?/, '')
    .split('/')
    .filter(Boolean);
  const query = url.search;
  const method = req.method;

  const head = segments[0];
  if (!head) return notFound();
  const second = segments[1];
  const third = segments[2];

  // ── Officer-composed reads ────────────────────────────────────────────────────────────────────
  if (head === 'summary' && method === 'GET') return summary(cfg);
  if (head === 'lookups' && method === 'GET') return lookups(cfg);

  if (head === 'bootstrap' && method === 'GET') {
    return passthrough(await callUpstream(cfg, { path: '/api/v1/bootstrap', query }));
  }

  // `?key=invoice|estimate|payment` — the number the next document would get.
  if (head === 'next-number' && method === 'GET') {
    return passthrough(await callUpstream(cfg, { path: '/api/v1/next-number', query }));
  }

  if (head === 'search' && method === 'GET') {
    return passthrough(await callUpstream(cfg, { path: '/api/v1/search', query }));
  }

  // ── Reports: PDFs on web routes keyed by the company hash ─────────────────────────────────────
  if (head === 'reports') {
    if (method !== 'GET' || !second) return notFound();
    const route = REPORTS[second];
    if (!route) return notFound();
    const hash = await resolveCompanyHash(cfg);
    return servePdf({ cfg, path: `/${route}/${hash}`, query, filename: `${second}.pdf` });
  }

  // ── Allow-listed resources ────────────────────────────────────────────────────────────────────
  const caps = RESOURCES[head];
  if (!caps) return notFound();

  const base = `/api/v1/${head}`;
  const contentType = req.headers.get('content-type') ?? 'application/json';

  // Collection: list and create.
  if (!second) {
    if (method === 'GET') return passthrough(await callUpstream(cfg, { path: base, query }));
    if (method === 'POST') {
      if (!caps.write) return badRequest(`${head} is read-only`);
      return passthrough(
        await callUpstream(cfg, { path: base, method: 'POST', body: await req.arrayBuffer(), contentType }),
      );
    }
    return notFound();
  }

  // Bulk delete — InvoiceShelf models it as POST /{resource}/delete with {"ids":[…]}, not DELETE.
  if (second === 'delete' && method === 'POST') {
    if (!caps.bulkDelete) return badRequest(`${head} has no bulk delete`);
    const body = await readJson(req);
    const ids = (body as { ids?: unknown })?.ids;
    if (!Array.isArray(ids) || ids.length === 0) return badRequest('ids must be a non-empty array');
    return passthrough(
      await callUpstream(cfg, {
        path: `${base}/delete`,
        method: 'POST',
        body: JSON.stringify({ ids }),
        contentType: 'application/json',
      }),
    );
  }

  // The template galleries, which sit alongside the collection rather than under an id.
  if (second === 'templates' && method === 'GET' && (head === 'invoices' || head === 'estimates')) {
    return passthrough(await callUpstream(cfg, { path: `${base}/templates` }));
  }

  const id = second;

  // Single resource.
  if (!third) {
    if (method === 'GET') return passthrough(await callUpstream(cfg, { path: `${base}/${id}`, query }));
    if (method === 'PUT' || method === 'PATCH') {
      if (!caps.write) return badRequest(`${head} is read-only`);
      return passthrough(
        await callUpstream(cfg, { path: `${base}/${id}`, method: 'PUT', body: await req.arrayBuffer(), contentType }),
      );
    }
    if (method === 'DELETE') {
      if (!caps.write) return badRequest(`${head} is read-only`);
      return passthrough(await callUpstream(cfg, { path: `${base}/${id}`, method: 'DELETE' }));
    }
    return notFound();
  }

  // ── Sub-resources ─────────────────────────────────────────────────────────────────────────────
  if (third === 'pdf' && method === 'GET') {
    if (!PDF_RESOURCES.has(head)) return notFound();
    const hash = await fetchUniqueHash(cfg, head, id);
    if (!hash) return Response.json({ error: `${head}/${id} not found` }, { status: 404 });
    // Built from OUR base rather than the upstream's `*_pdf_url`, which is rendered from the instance's
    // APP_URL and can point somewhere this process cannot reach.
    return servePdf({ cfg, path: `/${head}/pdf/${hash}`, query, filename: `${head.slice(0, -1)}-${id}.pdf` });
  }

  // The rendered email body, as HTML, without sending anything.
  if (third === 'preview' && method === 'GET') {
    if (!ACTIONS[head]?.includes('send')) return notFound();
    return passthrough(await callUpstream(cfg, { path: `${base}/${id}/send/preview`, query }));
  }

  if (head === 'customers' && third === 'stats' && method === 'GET') {
    return passthrough(await callUpstream(cfg, { path: `${base}/${id}/stats`, query }));
  }

  // Named actions. `send` really does email the customer, which is why actions are an explicit allow-list
  // per resource rather than a wildcard under the id.
  if (method === 'POST' && ACTIONS[head]?.includes(third)) {
    return passthrough(
      await callUpstream(cfg, {
        path: `${base}/${id}/${third}`,
        method: 'POST',
        body: await req.arrayBuffer(),
        contentType,
      }),
    );
  }

  return notFound();
}
