import { IMPORT_ENTITIES, IMPORT_ENTITY_DEFINITIONS, type ImportEntity, type Team } from '@pig/core';
import type { Database } from '@pig/db';
import { Hono } from 'hono';
import { z } from 'zod';
import { requireAnyTeamCapability, requireCapability } from '../lib/auth';
import type { ApiEnv, MutationDefinition } from '../lib/mutation';
import { MutationError, mutation } from '../lib/mutation';
import {
  ImportService,
  type ImportCommitResult,
  type ImportTransaction,
} from '../services/imports';
import {
  MAX_IMPORT_CELL_CHARS,
  MAX_IMPORT_COLUMNS,
  MAX_IMPORT_FILE_BYTES,
  MAX_IMPORT_ROWS,
  parseTabularFile,
} from '../services/tabular-import';

const planSchema = z.object({
  entity: z.enum(IMPORT_ENTITIES),
  sourceName: z.string().trim().min(1).max(255),
  headers: z.array(z.string().min(1).max(255)).min(1).max(MAX_IMPORT_COLUMNS),
  rows: z.array(
    z.array(z.string().max(MAX_IMPORT_CELL_CHARS)).max(MAX_IMPORT_COLUMNS),
  ).min(1).max(MAX_IMPORT_ROWS),
  mapping: z.record(z.string().min(1).max(255)),
  keySourceColumn: z.string().min(1).max(255),
}).strict();
const commitSchema = planSchema.extend({
  previewDigest: z.string().regex(/^[0-9a-f]{64}$/),
}).strict();
const parseSchema = z.object({
  fileName: z.string().trim().min(1).max(255),
  mimeType: z.string().max(255).optional(),
  base64: z.string().min(1).max(Math.ceil(MAX_IMPORT_FILE_BYTES * 4 / 3) + 16),
}).strict();

/**
 * Which team's book an import writes into.
 *
 * The commit is the only point at which that is knowable, and it is the only
 * point at which it matters: `requireCapability(p, 'data:import')` with no team
 * passed if the principal held the capability on *any* team, so a research-team
 * admin could rewrite the demand pipeline. Accounts and contacts are shared by
 * both commercial sides, so admin of either is enough for those.
 */
export const IMPORT_ENTITY_TEAMS: Readonly<Record<ImportEntity, readonly Team[]>> = {
  account: ['supply', 'demand'],
  contact: ['supply', 'demand'],
  demand_deal: ['demand'],
  supply_deal: ['supply'],
};

function requireImportPermission(
  principal: Parameters<typeof requireCapability>[0],
  entity: ImportEntity,
): void {
  const teams = IMPORT_ENTITY_TEAMS[entity];
  let denial: unknown;
  for (const team of teams) {
    try {
      requireCapability(principal, 'data:import', team);
      return;
    } catch (error) {
      denial = error;
    }
  }
  throw denial;
}

interface ImportCommitOperations {
  commit(
    input: z.infer<typeof commitSchema>,
    principal: Parameters<MutationDefinition<typeof commitSchema, ImportCommitResult>['mutate']>[0]['principal'],
    now: Date,
  ): Promise<ImportCommitResult>;
}
type ServiceFactory = (tx: ImportTransaction) => ImportCommitOperations;
const service: ServiceFactory = (tx) => new ImportService(tx);

export function createImportCommitMutationDefinition(
  makeService: ServiceFactory = service,
): MutationDefinition<typeof commitSchema, ImportCommitResult> {
  return {
    schema: commitSchema,
    // Two stages: any-team up front so a principal with no import authority at
    // all cannot probe the schema, then the entity's own team once the body has
    // been parsed and the target is finally knowable.
    permission: { authorize: (principal) => requireAnyTeamCapability(principal, 'data:import') },
    invalidMessage: 'Invalid import commit.',
    async mutate({ input, principal, tx, now }) {
      requireImportPermission(principal, input.entity);
      const result = await makeService(tx).commit(input, principal, now);
      const entityLabel = IMPORT_ENTITY_DEFINITIONS[input.entity].label.toLocaleLowerCase();
      return {
        data: result,
        activity: {
          type: 'note',
          subject: `Imported ${result.total} ${entityLabel}`,
          meta: {
            action: 'import',
            entity: input.entity,
            sourceName: input.sourceName,
            keySourceColumn: input.keySourceColumn,
            created: result.created,
            updated: result.updated,
          },
        },
      };
    },
  };
}

export function createImportRoutes(db: Database): Hono<ApiEnv> {
  const routes = new Hono<ApiEnv>();
  // Any team, deliberately: config, parse and preview touch no book at all —
  // preview is a dry run against uploaded cells. The commit is where the team
  // is enforced, because the commit is where rows are written.
  //
  // The nested integration namespaces are skipped rather than left to fall
  // through this. They are mounted after this router, so Hono runs this
  // middleware for them too, and it would have re-imposed `data:import` on the
  // OAuth routes that were just split onto `integration:connect` — the split
  // would have compiled, passed its unit tests, and changed nothing.
  routes.use('/api/imports/*', async (context, next) => {
    const path = new URL(context.req.url).pathname;
    if (path.startsWith('/api/imports/google/') || path.startsWith('/api/imports/notion/')) {
      return next();
    }
    requireAnyTeamCapability(context.get('principal'), 'data:import');
    await next();
  });
  routes.get('/api/imports/config', (context) => context.json({
    entities: IMPORT_ENTITIES,
    definitions: IMPORT_ENTITY_DEFINITIONS,
    limits: {
      fileBytes: MAX_IMPORT_FILE_BYTES,
      rows: MAX_IMPORT_ROWS,
      columns: MAX_IMPORT_COLUMNS,
      cellCharacters: MAX_IMPORT_CELL_CHARS,
    },
  }));
  routes.post('/api/imports/parse', async (context) => {
    const parsed = parseSchema.safeParse(await context.req.json());
    if (!parsed.success) throw new MutationError('invalid_import_file', 'Invalid import file upload.', 400, parsed.error.issues);
    let bytes: Uint8Array;
    try {
      bytes = Buffer.from(parsed.data.base64, 'base64');
    } catch {
      throw new MutationError('invalid_import_file', 'The import file encoding is invalid.', 400);
    }
    try {
      return context.json(parseTabularFile({
        fileName: parsed.data.fileName,
        mimeType: parsed.data.mimeType,
        bytes,
      }));
    } catch (error) {
      throw new MutationError(
        'invalid_import_file',
        error instanceof Error ? error.message : 'The import file could not be parsed.',
        400,
      );
    }
  });
  routes.post('/api/imports/preview', async (context) => {
    const parsed = planSchema.safeParse(await context.req.json());
    if (!parsed.success) throw new MutationError('invalid_import', 'Invalid import plan.', 400, parsed.error.issues);
    return context.json(await new ImportService(db).preview(parsed.data));
  });
  routes.post('/api/imports/commit', mutation(db, createImportCommitMutationDefinition()));
  return routes;
}
