import "server-only";
import http from "node:http";
import https from "node:https";

type MailcowJson =
  | Record<string, unknown>
  | Array<Record<string, unknown> | string | number | boolean | null>
  | string
  | number
  | boolean
  | null;

export type MailcowProvisioningResult = {
  dkimPublicKey: string;
  dkimSelector: string;
};

type MailcowConfig = {
  apiKey: string;
  allowSelfSigned: boolean;
  baseUrl: string;
  domainAliasLimit: number;
  domainMailboxLimit: number;
  domainQuotaMb: number;
  dkimKeySize: number;
  dkimSelector: string;
  mailboxQuotaMb: number;
  requestTimeoutMs: number;
};

type MailcowDomainRecord = {
  active?: string | number | boolean;
  domain_name?: string;
};

type MailcowMailboxRecord = {
  active?: string | number | boolean;
  percent_in_use?: string | number | null;
  quota?: string | number | null;
  quota_used?: string | number | null;
  username?: string;
};

type MailcowDkimRecord = {
  dkim_selector?: string;
  pubkey?: string;
};

export type MailcowMailboxQuotaSnapshot = {
  isUnlimited: boolean;
  limitBytes: number | null;
  usagePercent: number | null;
  usedBytes: number | null;
};

export type MailcowPostfixLogEntry = {
  message: string;
  time: number | string | null;
};

// Mailcow ignores quota=0 on domain edits, so use a high cap for app-level "no domain limit".
const UNLIMITED_DOMAIN_QUOTA_MB = 10 * 1024 * 1024;
const UNLIMITED_DOMAIN_MAILBOX_LIMIT = 10_000;

function readRequiredEnv(name: string, fallback?: string) {
  const value = (process.env[name] ?? fallback ?? "").trim();

  if (!value) {
    throw new Error("mailcow-config-missing");
  }

  return value;
}

function readIntegerEnv(name: string, fallback: number) {
  const rawValue = process.env[name];

  if (!rawValue) {
    return fallback;
  }

  const parsed = Number(rawValue);

  if (!Number.isInteger(parsed) || parsed <= 0) {
    throw new Error("mailcow-config-invalid");
  }

  return parsed;
}

function readNonNegativeIntegerEnv(name: string, fallback: number) {
  const rawValue = process.env[name];

  if (!rawValue) {
    return fallback;
  }

  const parsed = Number(rawValue);

  if (!Number.isInteger(parsed) || parsed < 0) {
    throw new Error("mailcow-config-invalid");
  }

  return parsed;
}

function readBooleanEnv(name: string, fallback = false) {
  const rawValue = process.env[name];

  if (!rawValue) {
    return fallback;
  }

  switch (rawValue.trim().toLowerCase()) {
    case "1":
    case "true":
    case "yes":
    case "on":
      return true;
    case "0":
    case "false":
    case "no":
    case "off":
      return false;
    default:
      throw new Error("mailcow-config-invalid");
  }
}

function getMailcowConfig(): MailcowConfig {
  const mailboxQuotaMb = readIntegerEnv("MAILCOW_MAILBOX_QUOTA_MB", 2048);
  const configuredDomainMailboxLimit = readNonNegativeIntegerEnv(
    "MAILCOW_DOMAIN_MAILBOX_LIMIT",
    0,
  );
  const domainMailboxLimit =
    configuredDomainMailboxLimit === 0
      ? UNLIMITED_DOMAIN_MAILBOX_LIMIT
      : configuredDomainMailboxLimit;
  const configuredDomainQuotaMb = readNonNegativeIntegerEnv("MAILCOW_DOMAIN_QUOTA_MB", 0);
  const domainQuotaMb =
    configuredDomainQuotaMb === 0
      ? UNLIMITED_DOMAIN_QUOTA_MB
      : configuredDomainQuotaMb;

  return {
    apiKey: readRequiredEnv("MAILCOW_API_KEY"),
    allowSelfSigned: readBooleanEnv("MAILCOW_ALLOW_SELF_SIGNED", false),
    baseUrl: readRequiredEnv("MAILCOW_BASE_URL", process.env.NEXT_PUBLIC_MAIL_APP_URL),
    domainAliasLimit: readIntegerEnv("MAILCOW_DOMAIN_ALIAS_LIMIT", 400),
    domainMailboxLimit,
    domainQuotaMb,
    dkimKeySize: readIntegerEnv("MAILCOW_DKIM_KEY_SIZE", 2048),
    dkimSelector: readRequiredEnv("DKIM_SELECTOR", "om1"),
    mailboxQuotaMb,
    requestTimeoutMs: readIntegerEnv("MAILCOW_REQUEST_TIMEOUT_MS", 15000),
  };
}

function normalizeMailcowMessage(payload: MailcowJson) {
  const messages = new Set<string>();

  const collect = (value: unknown) => {
    if (typeof value === "string" && value.trim()) {
      messages.add(value.trim());
      return;
    }

    if (Array.isArray(value)) {
      value.forEach(collect);
      return;
    }

    if (value && typeof value === "object") {
      const record = value as Record<string, unknown>;
      collect(record.msg);
      collect(record.message);
      collect(record.error);
      collect(record.detail);
    }
  };

  collect(payload);

  return [...messages].join(" | ");
}

function payloadHasMutationError(payload: MailcowJson) {
  if (Array.isArray(payload)) {
    return payload.some((item) => {
      if (!item || typeof item !== "object" || Array.isArray(item)) {
        return false;
      }

      const type = item.type;
      return type === "danger" || type === "error";
    });
  }

  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
    return false;
  }

  const type = payload.type;
  return type === "danger" || type === "error";
}

function buildMailcowUrl(pathname: string, baseUrl: string) {
  const url = new URL(baseUrl);
  url.pathname = pathname;
  url.search = "";
  return url;
}

function readOptionalNumber(value: unknown) {
  if (typeof value === "number" && Number.isFinite(value)) {
    return value;
  }

  if (typeof value === "string") {
    const normalized = value.trim();

    if (!normalized || normalized === "-") {
      return null;
    }

    const parsed = Number(normalized);
    return Number.isFinite(parsed) ? parsed : null;
  }

  return null;
}

async function performNodeRequest(
  url: URL,
  init: RequestInit,
  config: MailcowConfig,
) {
  const body =
    typeof init.body === "string"
      ? init.body
      : init.body
        ? String(init.body)
        : undefined;
  const headers = Object.fromEntries(new Headers(init.headers).entries());
  const isHttps = url.protocol === "https:";

  return new Promise<{ statusCode: number; body: string }>((resolve, reject) => {
    const request = (isHttps ? https : http).request(
      {
        protocol: url.protocol,
        hostname: url.hostname,
        port: url.port ? Number(url.port) : isHttps ? 443 : 80,
        path: `${url.pathname}${url.search}`,
        method: init.method ?? "GET",
        headers,
        rejectUnauthorized: isHttps ? !config.allowSelfSigned : undefined,
      },
      (response) => {
        let responseBody = "";

        response.setEncoding("utf8");
        response.on("data", (chunk) => {
          responseBody += chunk;
        });
        response.on("end", () => {
          resolve({
            statusCode: response.statusCode ?? 0,
            body: responseBody,
          });
        });
      },
    );

    request.setTimeout(config.requestTimeoutMs, () => {
      request.destroy(new Error("Mailcow request timed out"));
    });
    request.on("error", reject);

    if (body) {
      request.write(body);
    }

    request.end();
  });
}

async function requestMailcow(
  pathname: string,
  init: RequestInit,
  config = getMailcowConfig(),
): Promise<MailcowJson> {
  const headers = new Headers(init.headers);
  headers.set("Accept", "application/json");
  headers.set("X-API-Key", config.apiKey);

  if (init.body) {
    headers.set("Content-Type", "application/json");
  }

  const url = buildMailcowUrl(pathname, config.baseUrl);
  let response: { statusCode: number; body: string };

  try {
    response = await performNodeRequest(url, { ...init, headers }, config);
  } catch (error) {
    const message = error instanceof Error ? error.message : "Mailcow network request failed";
    throw new Error(`mailcow-request-failed:${message}`);
  }

  const text = response.body;
  let payload: MailcowJson = null;

  if (text) {
    try {
      payload = JSON.parse(text) as MailcowJson;
    } catch {
      payload = text;
    }
  }

  if (response.statusCode < 200 || response.statusCode >= 300) {
    const message = normalizeMailcowMessage(payload) || `HTTP ${response.statusCode}` || "Mailcow request failed";

    if (response.statusCode === 401 || response.statusCode === 403) {
      throw new Error("mailcow-auth-failed");
    }

    throw new Error(`mailcow-request-failed:${message}`);
  }

  return payload;
}

function findRecord<T extends Record<string, unknown>>(
  payload: MailcowJson,
  predicate: (record: Record<string, unknown>) => boolean,
) {
  if (Array.isArray(payload)) {
    for (const item of payload) {
      if (!item || typeof item !== "object" || Array.isArray(item)) {
        continue;
      }

      if (predicate(item)) {
        return item as T;
      }
    }

    return null;
  }

  if (payload && typeof payload === "object" && !Array.isArray(payload) && predicate(payload)) {
    return payload as T;
  }

  return null;
}

async function getMailcowDomain(domain: string, config = getMailcowConfig()) {
  const payload = await requestMailcow(`/api/v1/get/domain/${encodeURIComponent(domain)}`, { method: "GET" }, config);

  return findRecord<MailcowDomainRecord>(payload, (record) => record.domain_name === domain);
}

async function getMailcowMailbox(mailboxEmail: string, config = getMailcowConfig()) {
  const payload = await requestMailcow(
    `/api/v1/get/mailbox/${encodeURIComponent(mailboxEmail)}`,
    { method: "GET" },
    config,
  );

  return findRecord<MailcowMailboxRecord>(payload, (record) => record.username === mailboxEmail);
}

export async function getMailcowMailboxQuota(
  mailboxEmail: string,
  config = getMailcowConfig(),
): Promise<MailcowMailboxQuotaSnapshot | null> {
  const mailbox = await getMailcowMailbox(mailboxEmail, config);

  if (!mailbox) {
    return null;
  }

  const rawLimitBytes = readOptionalNumber(mailbox.quota);
  const usedBytes = readOptionalNumber(mailbox.quota_used);
  const isUnlimited = rawLimitBytes === 0;
  const limitBytes = rawLimitBytes === null || rawLimitBytes === 0 ? null : rawLimitBytes;
  const rawUsagePercent = readOptionalNumber(mailbox.percent_in_use);
  const usagePercent =
    rawUsagePercent !== null
      ? Math.max(0, Math.min(100, rawUsagePercent))
      : usedBytes !== null && limitBytes && limitBytes > 0
        ? Math.max(0, Math.min(100, Math.round((usedBytes / limitBytes) * 100)))
        : null;

  return {
    isUnlimited,
    limitBytes,
    usagePercent,
    usedBytes,
  };
}

export async function getMailcowPostfixLogs(count = 500) {
  const normalizedCount = Math.max(1, Math.min(5000, Math.floor(count)));
  const payload = await requestMailcow(`/api/v1/get/logs/postfix/${normalizedCount}`, { method: "GET" });

  if (!Array.isArray(payload)) {
    return [] satisfies MailcowPostfixLogEntry[];
  }

  return payload
    .map((item) => {
      if (!item || typeof item !== "object" || Array.isArray(item)) {
        return null;
      }

      const message = typeof item.message === "string" ? item.message : "";

      if (!message) {
        return null;
      }

      return {
        message,
        time:
          typeof item.time === "string" || typeof item.time === "number"
            ? item.time
            : null,
      } satisfies MailcowPostfixLogEntry;
    })
    .filter((item): item is MailcowPostfixLogEntry => Boolean(item));
}

async function getMailcowDkim(domain: string, config = getMailcowConfig()) {
  const payload = await requestMailcow(`/api/v1/get/dkim/${encodeURIComponent(domain)}`, { method: "GET" }, config);

  return findRecord<MailcowDkimRecord>(
    payload,
    (record) => typeof record.pubkey === "string" && typeof record.dkim_selector === "string",
  );
}

async function upsertMailcowDomain(
  domain: string,
  allowExistingDomain: boolean,
  mailboxEmail: string,
  config = getMailcowConfig(),
) {
  const existingDomain = await getMailcowDomain(domain, config);
  const createPayload = {
    active: true,
    aliases: config.domainAliasLimit,
    backupmx: false,
    defquota: config.mailboxQuotaMb,
    description: `Provisioned by Official Mail (${domain})`,
    domain,
    mailboxes: config.domainMailboxLimit,
    maxquota: config.mailboxQuotaMb,
    quota: config.domainQuotaMb,
    relay_all_recipients: false,
    rl_frame: "s",
    rl_value: 10,
    restart_sogo: 1,
  };
  const editPayload = {
    active: true,
    aliases: config.domainAliasLimit,
    backupmx: false,
    defquota: config.mailboxQuotaMb,
    description: `Provisioned by Official Mail (${domain})`,
    mailboxes: config.domainMailboxLimit,
    maxquota: config.mailboxQuotaMb,
    quota: config.domainQuotaMb,
    relay_all_recipients: false,
  };

  if (!existingDomain) {
    const payload = await requestMailcow(
      "/api/v1/add/domain",
      { method: "POST", body: JSON.stringify(createPayload) },
      config,
    );

    if (payloadHasMutationError(payload) && !(await getMailcowDomain(domain, config))) {
      throw new Error(
        `mailcow-provisioning-failed:${normalizeMailcowMessage(payload) || "Unable to create domain in Mailcow"}`,
      );
    }

    return;
  }

  if (!allowExistingDomain) {
    const existingMailbox = await getMailcowMailbox(mailboxEmail, config);

    if (!existingMailbox) {
      throw new Error("mailcow-domain-exists");
    }
  }

  const payload = await requestMailcow(
    "/api/v1/edit/domain",
    {
      method: "POST",
      body: JSON.stringify({
        attr: editPayload,
        items: [domain],
      }),
    },
    config,
  );

  if (payloadHasMutationError(payload)) {
    throw new Error(
      `mailcow-provisioning-failed:${normalizeMailcowMessage(payload) || "Unable to update domain in Mailcow"}`,
    );
  }
}

export async function ensureMailcowDomain(input: {
  allowExistingDomain: boolean;
  domain: string;
  mailboxEmail: string;
}) {
  const config = getMailcowConfig();

  await upsertMailcowDomain(
    input.domain,
    input.allowExistingDomain,
    input.mailboxEmail,
    config,
  );
}

async function upsertMailcowMailbox(
  input: {
    displayName: string;
    domain: string;
    localPart: string;
    mailboxPassword: string;
  },
  config = getMailcowConfig(),
) {
  const mailboxEmail = `${input.localPart}@${input.domain}`;
  const existingMailbox = await getMailcowMailbox(mailboxEmail, config);
  const createPayload = {
    active: true,
    authsource: "mailcow",
    domain: input.domain,
    force_pw_update: false,
    local_part: input.localPart,
    name: input.displayName,
    password: input.mailboxPassword,
    password2: input.mailboxPassword,
    quota: config.mailboxQuotaMb,
    tls_enforce_in: false,
    tls_enforce_out: false,
  };
  const editPayload = {
    active: true,
    authsource: "mailcow",
    force_pw_update: false,
    name: input.displayName,
    password: input.mailboxPassword,
    password2: input.mailboxPassword,
    quota: config.mailboxQuotaMb,
  };

  if (!existingMailbox) {
    const payload = await requestMailcow(
      "/api/v1/add/mailbox",
      {
        method: "POST",
        body: JSON.stringify(createPayload),
      },
      config,
    );

    if (payloadHasMutationError(payload) && !(await getMailcowMailbox(mailboxEmail, config))) {
      throw new Error(
        `mailcow-provisioning-failed:${normalizeMailcowMessage(payload) || "Unable to create mailbox in Mailcow"}`,
      );
    }

    return;
  }

  const payload = await requestMailcow(
    "/api/v1/edit/mailbox",
    {
      method: "POST",
      body: JSON.stringify({
        attr: editPayload,
        items: [mailboxEmail],
      }),
    },
    config,
  );

  if (payloadHasMutationError(payload)) {
    throw new Error(
      `mailcow-provisioning-failed:${normalizeMailcowMessage(payload) || "Unable to update mailbox in Mailcow"}`,
    );
  }
}

export async function ensureMailcowMailbox(input: {
  displayName: string;
  domain: string;
  localPart: string;
  mailboxPassword: string;
}) {
  const config = getMailcowConfig();

  await upsertMailcowMailbox(input, config);
}

export async function ensureMailcowMailboxAccount(input: {
  displayName: string;
  domain: string;
  localPart: string;
  mailboxPassword: string;
}) {
  const config = getMailcowConfig();
  const mailboxEmail = `${input.localPart}@${input.domain}`;

  await upsertMailcowDomain(input.domain, true, mailboxEmail, config);
  await upsertMailcowMailbox(input, config);
}

export async function setMailcowMailboxActive(
  mailboxEmail: string,
  active: boolean,
  config = getMailcowConfig(),
) {
  const normalizedMailboxEmail = mailboxEmail.trim().toLowerCase();

  if (!normalizedMailboxEmail) {
    return false;
  }

  const existingMailbox = await getMailcowMailbox(normalizedMailboxEmail, config);

  if (!existingMailbox) {
    return false;
  }

  const payload = await requestMailcow(
    "/api/v1/edit/mailbox",
    {
      method: "POST",
      body: JSON.stringify({
        attr: { active },
        items: [normalizedMailboxEmail],
      }),
    },
    config,
  );

  if (payloadHasMutationError(payload)) {
    throw new Error(
      `mailcow-provisioning-failed:${normalizeMailcowMessage(payload) || "Unable to update mailbox in Mailcow"}`,
    );
  }

  return true;
}

export async function deleteMailcowMailboxAccount(
  mailboxEmail: string,
  config = getMailcowConfig(),
) {
  const normalizedMailboxEmail = mailboxEmail.trim().toLowerCase();

  if (!normalizedMailboxEmail) {
    return;
  }

  const existingMailbox = await getMailcowMailbox(normalizedMailboxEmail, config);

  if (!existingMailbox) {
    return;
  }

  const payload = await requestMailcow(
    "/api/v1/delete/mailbox",
    {
      method: "POST",
      body: JSON.stringify([normalizedMailboxEmail]),
    },
    config,
  );

  if (payloadHasMutationError(payload) && (await getMailcowMailbox(normalizedMailboxEmail, config))) {
    throw new Error(
      `mailcow-provisioning-failed:${normalizeMailcowMessage(payload) || "Unable to delete mailbox in Mailcow"}`,
    );
  }
}

export async function deleteMailcowDomain(domain: string) {
  const config = getMailcowConfig();
  const normalizedDomain = domain.trim().toLowerCase();

  if (!normalizedDomain) {
    throw new Error("mailcow-domain-required");
  }

  const existingDkim = await getMailcowDkim(normalizedDomain, config);

  if (existingDkim) {
    const dkimPayload = await requestMailcow(
      "/api/v1/delete/dkim",
      {
        method: "POST",
        body: JSON.stringify([normalizedDomain]),
      },
      config,
    );

    if (payloadHasMutationError(dkimPayload) && (await getMailcowDkim(normalizedDomain, config))) {
      throw new Error(
        `mailcow-provisioning-failed:${normalizeMailcowMessage(dkimPayload) || "Unable to delete DKIM in Mailcow"}`,
      );
    }
  }

  const existingDomain = await getMailcowDomain(normalizedDomain, config);

  if (!existingDomain) {
    return;
  }

  const payload = await requestMailcow(
    "/api/v1/delete/domain",
    {
      method: "POST",
      body: JSON.stringify([normalizedDomain]),
    },
    config,
  );

  if (payloadHasMutationError(payload) && (await getMailcowDomain(normalizedDomain, config))) {
    throw new Error(
      `mailcow-provisioning-failed:${normalizeMailcowMessage(payload) || "Unable to delete domain in Mailcow"}`,
    );
  }
}

async function ensureMailcowDkim(
  domain: string,
  config = getMailcowConfig(),
  dkimKeySize = config.dkimKeySize,
) {
  const existingDkim = await getMailcowDkim(domain, config);

  if (existingDkim?.pubkey && existingDkim.dkim_selector) {
    return {
      dkimPublicKey: existingDkim.pubkey,
      dkimSelector: existingDkim.dkim_selector,
    } satisfies MailcowProvisioningResult;
  }

  return createMailcowDkim(domain, config, dkimKeySize);
}

async function createMailcowDkim(
  domain: string,
  config: MailcowConfig,
  dkimKeySize: number,
  dkimSelector = config.dkimSelector,
) {
  const payload = await requestMailcow(
    "/api/v1/add/dkim",
    {
      method: "POST",
      body: JSON.stringify({
        dkim_selector: dkimSelector,
        domains: domain,
        key_size: dkimKeySize,
      }),
    },
    config,
  );

  if (payloadHasMutationError(payload)) {
    throw new Error(
      `mailcow-provisioning-failed:${normalizeMailcowMessage(payload) || "Unable to generate DKIM in Mailcow"}`,
    );
  }

  const dkimRecord = await getMailcowDkim(domain, config);

  if (!dkimRecord?.pubkey || !dkimRecord.dkim_selector) {
    throw new Error("mailcow-provisioning-failed:DKIM key not returned by Mailcow");
  }

  return {
    dkimPublicKey: dkimRecord.pubkey,
    dkimSelector: dkimRecord.dkim_selector,
  } satisfies MailcowProvisioningResult;
}

export async function ensureMailcowDkimRecord(domain: string) {
  const config = getMailcowConfig();

  return ensureMailcowDkim(domain, config);
}

export async function regenerateMailcowDkim(
  domain: string,
  dkimKeySize: 1024 | 2048,
) {
  const config = getMailcowConfig();
  const normalizedDomain = domain.trim().toLowerCase();

  if (!normalizedDomain) {
    throw new Error("mailcow-domain-required");
  }

  const existingDkim = await getMailcowDkim(normalizedDomain, config);
  const dkimSelector = existingDkim?.dkim_selector || config.dkimSelector;

  if (existingDkim) {
    const payload = await requestMailcow(
      "/api/v1/delete/dkim",
      {
        method: "POST",
        body: JSON.stringify([normalizedDomain]),
      },
      config,
    );

    if (payloadHasMutationError(payload)) {
      throw new Error(
        `mailcow-provisioning-failed:${normalizeMailcowMessage(payload) || "Unable to replace DKIM in Mailcow"}`,
      );
    }
  }

  // Reissue must create a new key instead of reusing a stale read after deletion.
  return createMailcowDkim(
    normalizedDomain,
    config,
    dkimKeySize,
    dkimSelector,
  );
}

export async function ensureMailcowProvisioning(input: {
  allowExistingDomain: boolean;
  dkimKeySize?: 1024 | 2048;
  displayName: string;
  domain: string;
  localPart: string;
  mailboxPassword: string;
}) {
  const config = getMailcowConfig();
  const mailboxEmail = `${input.localPart}@${input.domain}`;

  await upsertMailcowDomain(input.domain, input.allowExistingDomain, mailboxEmail, config);
  await upsertMailcowMailbox(
    {
      displayName: input.displayName,
      domain: input.domain,
      localPart: input.localPart,
      mailboxPassword: input.mailboxPassword,
    },
    config,
  );

  return ensureMailcowDkim(input.domain, config, input.dkimKeySize);
}

export async function isMailcowProvisioned(domain: string, mailboxEmail: string) {
  const config = getMailcowConfig();
  const [domainRecord, mailboxRecord] = await Promise.all([
    getMailcowDomain(domain, config),
    getMailcowMailbox(mailboxEmail, config),
  ]);

  return Boolean(domainRecord && mailboxRecord);
}
