import type { Context, Middleware } from "./context.ts";

export interface Tenant {
  id: string;
  slug?: string;
  name?: string;
  metadata?: Record<string, unknown>;
}

export interface TenantResource {
  tenantId: string;
}
export interface TenantMembership {
  tenantId: string;
  userId: string;
  roles?: string[];
  workspaceIds?: string[];
}
export interface TenantAuditEvent {
  tenantId: string;
  action: string;
  actorId?: string;
  resource?: string;
  metadata?: Record<string, unknown>;
  createdAt: number;
}

export interface TenantQuota {
  tenantId: string;
  resource: string;
  limit: number;
  usage: number;
}
export interface TenantDirectoryStore {
  putMembership(value: TenantMembership): Promise<void>;
  getMembership(tenantId: string, userId: string): Promise<TenantMembership | null>;
  listMemberships(tenantId: string): Promise<TenantMembership[]>;
  putQuota(value: TenantQuota): Promise<void>;
  getQuota(tenantId: string, resource: string): Promise<TenantQuota | null>;
}

export type TenantResolver = (ctx: Context) => Tenant | null | Promise<Tenant | null>;

export interface TenantMiddlewareOptions {
  required?: boolean;
  status?: number;
}

export function tenantMiddleware(
  resolveTenant: TenantResolver,
  options: TenantMiddlewareOptions = {},
): Middleware {
  return async (ctx, next) => {
    const tenant = await resolveTenant(ctx);
    ctx.tenant = tenant ?? undefined;
    if (!tenant && options.required !== false) {
      return new Response("Tenant not found", { status: options.status ?? 404 });
    }
    return next();
  };
}

export function tenantFromSubdomain(
  lookup: (slug: string, ctx: Context) => Tenant | null | Promise<Tenant | null>,
  rootDomains: string[] = [],
): TenantResolver {
  return async (ctx) => {
    const host = ctx.url.hostname.toLowerCase();
    const root = rootDomains.find((domain) => host === domain || host.endsWith(`.${domain}`));
    const slug = root ? host.slice(0, -(root.length + 1)) : host.split(".")[0];
    if (!slug || slug === host || slug === "www") return null;
    return lookup(slug, ctx);
  };
}

export function tenantFromDomain(
  lookup: (domain: string, ctx: Context) => Tenant | null | Promise<Tenant | null>,
): TenantResolver {
  return (ctx) => lookup(ctx.url.hostname.toLowerCase(), ctx);
}

export function tenantFromPath(
  lookup: (slug: string, ctx: Context) => Tenant | null | Promise<Tenant | null>,
  prefix = "",
): TenantResolver {
  return (ctx) => {
    const segments = ctx.url.pathname.split("/").filter(Boolean);
    const normalized = prefix.replace(/^\/+|\/+$/g, "");
    const slug = normalized ? (segments[0] === normalized ? segments[1] : undefined) : segments[0];
    return slug ? lookup(slug, ctx) : null;
  };
}

/** Header resolution is intentionally opt-in and must only be used behind a trusted proxy. */
export function tenantFromHeader(
  lookup: (id: string, ctx: Context) => Tenant | null | Promise<Tenant | null>,
  header = "x-wrnexus-tenant",
): TenantResolver {
  return (ctx) => {
    const value = ctx.req.headers.get(header)?.trim();
    return value ? lookup(value, ctx) : null;
  };
}

export function tenantFromSession(
  resolveId: (ctx: Context) => string | null | Promise<string | null>,
  lookup: (id: string, ctx: Context) => Tenant | null | Promise<Tenant | null>,
): TenantResolver {
  return async (ctx) => {
    const id = await resolveId(ctx);
    return id ? lookup(id, ctx) : null;
  };
}

export function composeTenantResolvers(...resolvers: TenantResolver[]): TenantResolver {
  return async (ctx) => {
    for (const resolver of resolvers) {
      const tenant = await resolver(ctx);
      if (tenant) return tenant;
    }
    return null;
  };
}

export function requireTenant(ctx: Context): Tenant {
  if (!ctx.tenant)
    throw new Error("WRN-TENANT-REQUIRED: tenant middleware has not resolved a tenant.");
  return ctx.tenant;
}

/** Wrap a repository so every operation receives the current tenant id. */
export function tenantScope<T extends object>(
  tenant: Tenant,
  repository: T,
): T & { tenantId: string } {
  return Object.assign(Object.create(repository), { tenantId: tenant.id });
}

export function assertTenantAccess(tenant: Tenant, resource: TenantResource): void {
  if (!resource.tenantId || resource.tenantId !== tenant.id)
    throw new Error("WRN-TENANT-CROSS-ACCESS: resource does not belong to the active tenant.");
}

export function tenantKey(tenant: Tenant | string, ...parts: Array<string | number>): string {
  const id = typeof tenant === "string" ? tenant : tenant.id;
  if (!id.trim() || id.includes(":"))
    throw new TypeError("WRN-TENANT-KEY: tenant id must be non-empty and cannot contain ':'.");
  return [
    "tenant",
    encodeURIComponent(id),
    ...parts.map((part) => encodeURIComponent(String(part))),
  ].join(":");
}

export function createTenantDirectory(
  options: { audit?: (event: TenantAuditEvent) => void | Promise<void>; now?: () => number } = {},
) {
  const memberships = new Map<string, TenantMembership>();
  const quotas = new Map<string, Map<string, number>>();
  const now = options.now ?? Date.now;
  const key = (tenantId: string, userId: string) => `${tenantId}\0${userId}`;
  return {
    async addMembership(membership: TenantMembership, actorId?: string) {
      if (!membership.tenantId || !membership.userId)
        throw new TypeError("tenantId and userId are required");
      memberships.set(key(membership.tenantId, membership.userId), structuredClone(membership));
      await options.audit?.({
        tenantId: membership.tenantId,
        action: "membership.added",
        actorId,
        resource: membership.userId,
        createdAt: now(),
      });
    },
    membership(tenantId: string, userId: string) {
      const value = memberships.get(key(tenantId, userId));
      return value ? structuredClone(value) : null;
    },
    async switchWorkspace(tenantId: string, userId: string, workspaceId: string) {
      const membership = memberships.get(key(tenantId, userId));
      if (!membership?.workspaceIds?.includes(workspaceId))
        throw new Error("WRN-TENANT-WORKSPACE-DENIED");
      await options.audit?.({
        tenantId,
        action: "workspace.switched",
        actorId: userId,
        resource: workspaceId,
        createdAt: now(),
      });
      return { tenantId, workspaceId };
    },
    setQuota(tenantId: string, resource: string, limit: number) {
      if (!Number.isFinite(limit) || limit < 0)
        throw new RangeError("tenant quota must be non-negative");
      const values = quotas.get(tenantId) ?? new Map();
      values.set(resource, limit);
      quotas.set(tenantId, values);
    },
    enforceQuota(tenantId: string, resource: string, usage: number, requested = 0) {
      const limit = quotas.get(tenantId)?.get(resource);
      if (limit !== undefined && usage + requested > limit)
        throw new Error(`WRN-TENANT-QUOTA: ${resource} quota exceeded.`);
      return { usage, requested, limit };
    },
  };
}

export function memoryTenantDirectoryStore(): TenantDirectoryStore {
  const memberships = new Map<string, TenantMembership>();
  const quotas = new Map<string, TenantQuota>();
  return {
    async putMembership(value) {
      memberships.set(`${value.tenantId}\0${value.userId}`, structuredClone(value));
    },
    async getMembership(tenantId, userId) {
      const value = memberships.get(`${tenantId}\0${userId}`);
      return value ? structuredClone(value) : null;
    },
    async listMemberships(tenantId) {
      return [...memberships.values()]
        .filter((value) => value.tenantId === tenantId)
        .map((value) => structuredClone(value));
    },
    async putQuota(value) {
      quotas.set(`${value.tenantId}\0${value.resource}`, structuredClone(value));
    },
    async getQuota(tenantId, resource) {
      const value = quotas.get(`${tenantId}\0${resource}`);
      return value ? structuredClone(value) : null;
    },
  };
}

export function createPersistentTenantDirectory(
  store: TenantDirectoryStore,
  options: { audit?: (event: TenantAuditEvent) => void | Promise<void>; now?: () => number } = {},
) {
  const now = options.now ?? Date.now;
  return {
    async addMembership(membership: TenantMembership, actorId?: string) {
      if (!membership.tenantId || !membership.userId)
        throw new TypeError("tenantId and userId are required");
      await store.putMembership(structuredClone(membership));
      await options.audit?.({
        tenantId: membership.tenantId,
        action: "membership.added",
        actorId,
        resource: membership.userId,
        createdAt: now(),
      });
    },
    membership: (tenantId: string, userId: string) => store.getMembership(tenantId, userId),
    memberships: (tenantId: string) => store.listMemberships(tenantId),
    async switchWorkspace(tenantId: string, userId: string, workspaceId: string) {
      const membership = await store.getMembership(tenantId, userId);
      if (!membership?.workspaceIds?.includes(workspaceId))
        throw new Error("WRN-TENANT-WORKSPACE-DENIED");
      await options.audit?.({
        tenantId,
        action: "workspace.switched",
        actorId: userId,
        resource: workspaceId,
        createdAt: now(),
      });
      return { tenantId, workspaceId };
    },
    async setQuota(tenantId: string, resource: string, limit: number, usage = 0) {
      if (!Number.isFinite(limit) || limit < 0 || !Number.isFinite(usage) || usage < 0)
        throw new RangeError("tenant quota values must be non-negative");
      await store.putQuota({ tenantId, resource, limit, usage });
    },
    async consumeQuota(tenantId: string, resource: string, requested: number) {
      if (!Number.isFinite(requested) || requested < 0)
        throw new RangeError("requested quota must be non-negative");
      const quota = await store.getQuota(tenantId, resource);
      if (quota && quota.usage + requested > quota.limit)
        throw new Error(`WRN-TENANT-QUOTA: ${resource} quota exceeded.`);
      if (quota) {
        quota.usage += requested;
        await store.putQuota(quota);
      }
      return quota;
    },
  };
}

export interface TenantSqlClient {
  query<T = Record<string, unknown>>(sql: string, parameters?: unknown[]): Promise<{ rows: T[] }>;
}
export function postgresTenantDirectoryStore(db: TenantSqlClient): TenantDirectoryStore {
  return {
    async putMembership(value) {
      await db.query(
        `INSERT INTO wrnexus_tenant_memberships (tenant_id,user_id,roles,workspace_ids) VALUES ($1,$2,$3,$4) ON CONFLICT (tenant_id,user_id) DO UPDATE SET roles=$3,workspace_ids=$4`,
        [
          value.tenantId,
          value.userId,
          JSON.stringify(value.roles ?? []),
          JSON.stringify(value.workspaceIds ?? []),
        ],
      );
    },
    async getMembership(tenantId, userId) {
      const result = await db.query<any>(
        `SELECT tenant_id AS "tenantId",user_id AS "userId",roles,workspace_ids AS "workspaceIds" FROM wrnexus_tenant_memberships WHERE tenant_id=$1 AND user_id=$2`,
        [tenantId, userId],
      );
      return result.rows[0] ?? null;
    },
    async listMemberships(tenantId) {
      const result = await db.query<TenantMembership>(
        `SELECT tenant_id AS "tenantId",user_id AS "userId",roles,workspace_ids AS "workspaceIds" FROM wrnexus_tenant_memberships WHERE tenant_id=$1 ORDER BY user_id`,
        [tenantId],
      );
      return result.rows;
    },
    async putQuota(value) {
      await db.query(
        `INSERT INTO wrnexus_tenant_quotas (tenant_id,resource,quota_limit,usage) VALUES ($1,$2,$3,$4) ON CONFLICT (tenant_id,resource) DO UPDATE SET quota_limit=$3,usage=$4`,
        [value.tenantId, value.resource, value.limit, value.usage],
      );
    },
    async getQuota(tenantId, resource) {
      const result = await db.query<any>(
        `SELECT tenant_id AS "tenantId",resource,quota_limit AS "limit",usage FROM wrnexus_tenant_quotas WHERE tenant_id=$1 AND resource=$2`,
        [tenantId, resource],
      );
      return result.rows[0] ?? null;
    },
  };
}

export const POSTGRES_TENANT_DIRECTORY_SCHEMA = `CREATE TABLE IF NOT EXISTS wrnexus_tenant_memberships (tenant_id text NOT NULL,user_id text NOT NULL,roles jsonb NOT NULL DEFAULT '[]',workspace_ids jsonb NOT NULL DEFAULT '[]',PRIMARY KEY (tenant_id,user_id)); CREATE TABLE IF NOT EXISTS wrnexus_tenant_quotas (tenant_id text NOT NULL,resource text NOT NULL,quota_limit bigint NOT NULL,usage bigint NOT NULL DEFAULT 0,PRIMARY KEY (tenant_id,resource));`;

export async function migrateTenants<T extends Tenant>(
  tenants: T[],
  migrate: (tenant: T) => void | Promise<void>,
  options: { concurrency?: number; continueOnError?: boolean } = {},
) {
  const concurrency = options.concurrency ?? 4;
  if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 32)
    throw new RangeError("Tenant migration concurrency must be between 1 and 32");
  const pending = [...tenants];
  const migrated: string[] = [];
  const failed: Array<{ tenantId: string; error: string }> = [];
  await Promise.all(
    Array.from({ length: Math.min(concurrency, pending.length) }, async () => {
      while (pending.length) {
        const tenant = pending.shift()!;
        try {
          await migrate(tenant);
          migrated.push(tenant.id);
        } catch (error) {
          failed.push({
            tenantId: tenant.id,
            error: error instanceof Error ? error.message : String(error),
          });
          if (!options.continueOnError) pending.length = 0;
        }
      }
    }),
  );
  return { migrated, failed };
}
