import fs from "node:fs";
import path from "node:path";
import { z } from "zod";

const booleanFromEnv = z.preprocess((value) => {
  if (typeof value !== "string") return value;
  if (["1", "true", "yes", "on"].includes(value.toLowerCase())) return true;
  if (["0", "false", "no", "off"].includes(value.toLowerCase())) return false;
  return value;
}, z.boolean());

const integerFromEnv = (minimum: number, maximum: number) =>
  z.coerce.number().int().min(minimum).max(maximum);

const optionalString = z.preprocess(
  (value) => (typeof value === "string" && value.trim() === "" ? undefined : value),
  z.string().optional(),
);

const EnvSchema = z
  .object({
    NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
    HOST: z.string().default("127.0.0.1"),
    PORT: integerFromEnv(1, 65_535).default(3000),
    BASE_URL: z.url(),
    MAILBOXES_CONFIG: z.string().default("config/mailboxes.json"),
    STATE_DIR: z.string().default("data"),

    MCP_PROFILE_ID: z.string().min(1).max(128),
    MCP_PROFILE_NAME: z.string().min(1).max(128).default("Mail MCP user"),
    MCP_PROFILE_EMAIL: optionalString.pipe(z.email().optional()),
    MCP_ADMIN_PASSWORD_HASH: z.string().startsWith("scrypt$") ,
    MCP_TOKEN_SECRET: z.string().min(40),

    OAUTH_ALLOW_DYNAMIC_REGISTRATION: booleanFromEnv.default(true),
    OAUTH_ALLOWED_REDIRECT_HOSTS: z.string().default("chatgpt.com,openai.com"),
    OAUTH_CLIENT_ID: optionalString,
    OAUTH_CLIENT_SECRET: optionalString,
    OAUTH_REDIRECT_URIS: optionalString,

    ENABLE_DRAFTS: booleanFromEnv.default(true),
    ENABLE_SEND: booleanFromEnv.default(false),
    MAX_SEARCH_RESULTS: integerFromEnv(1, 100).default(50),
    MAX_MESSAGE_BYTES: integerFromEnv(65_536, 50 * 1024 * 1024).default(8 * 1024 * 1024),
    MAX_ATTACHMENT_BYTES: integerFromEnv(65_536, 50 * 1024 * 1024).default(8 * 1024 * 1024),
    MAX_BODY_CHARS: integerFromEnv(1_000, 500_000).default(100_000),
    PREPARED_EMAIL_TTL_SECONDS: integerFromEnv(60, 3_600).default(900),
    ACCESS_TOKEN_TTL_SECONDS: integerFromEnv(300, 86_400).default(3_600),
    REFRESH_TOKEN_TTL_SECONDS: integerFromEnv(3_600, 90 * 86_400).default(30 * 86_400),
  })
  .superRefine((env, ctx) => {
    const baseUrl = new URL(env.BASE_URL);
    const isLocal = ["localhost", "127.0.0.1", "::1"].includes(baseUrl.hostname);
    if (baseUrl.protocol !== "https:" && !isLocal) {
      ctx.addIssue({
        code: "custom",
        path: ["BASE_URL"],
        message: "BASE_URL must use HTTPS outside localhost",
      });
    }
    if (baseUrl.pathname !== "/" || baseUrl.search || baseUrl.hash) {
      ctx.addIssue({
        code: "custom",
        path: ["BASE_URL"],
        message: "BASE_URL must be an origin without a path, query or fragment",
      });
    }

    const staticClientValues = [env.OAUTH_CLIENT_ID, env.OAUTH_CLIENT_SECRET, env.OAUTH_REDIRECT_URIS];
    const configuredValues = staticClientValues.filter(Boolean).length;
    if (configuredValues > 0 && configuredValues < staticClientValues.length) {
      ctx.addIssue({
        code: "custom",
        path: ["OAUTH_CLIENT_ID"],
        message:
          "OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET and OAUTH_REDIRECT_URIS must be set together",
      });
    }
    if (!env.OAUTH_ALLOW_DYNAMIC_REGISTRATION && configuredValues === 0) {
      ctx.addIssue({
        code: "custom",
        path: ["OAUTH_ALLOW_DYNAMIC_REGISTRATION"],
        message: "A predefined OAuth client is required when dynamic registration is disabled",
      });
    }
  });

const MailServerSchema = z.object({
  host: z.string().min(1),
  port: z.number().int().min(1).max(65_535),
  secure: z.boolean(),
  user: z.string().min(1),
  passwordEnv: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
  servername: z.string().min(1).optional(),
});

const MailboxSchema = z.object({
  id: z.string().regex(/^[a-z0-9][a-z0-9_-]{1,63}$/),
  label: z.string().min(1).max(128),
  address: z.email(),
  aliases: z.array(z.email()).default([]),
  imap: MailServerSchema,
  smtp: MailServerSchema.optional(),
  folders: z
    .object({
      sent: z.string().min(1).optional(),
      drafts: z.string().min(1).optional(),
    })
    .default({}),
});

const MailboxesFileSchema = z
  .object({
    mailboxes: z.array(MailboxSchema).min(1).max(50),
  })
  .superRefine((value, ctx) => {
    const ids = new Set<string>();
    for (const [index, mailbox] of value.mailboxes.entries()) {
      if (ids.has(mailbox.id)) {
        ctx.addIssue({
          code: "custom",
          path: ["mailboxes", index, "id"],
          message: `Duplicate mailbox id: ${mailbox.id}`,
        });
      }
      ids.add(mailbox.id);
    }
  });

export type MailboxConfig = z.infer<typeof MailboxSchema> & {
  imapPassword: string;
  smtpPassword?: string;
};

export type AppConfig = {
  nodeEnv: "development" | "test" | "production";
  host: string;
  port: number;
  baseUrl: URL;
  mcpUrl: URL;
  mailboxesPath: string;
  stateDir: string;
  profile: {
    id: string;
    name: string;
    email?: string;
  };
  adminPasswordHash: string;
  tokenSecret: Uint8Array;
  oauth: {
    allowDynamicRegistration: boolean;
    allowedRedirectHosts: string[];
    staticClient?: {
      clientId: string;
      clientSecret: string;
      redirectUris: string[];
    };
  };
  features: {
    drafts: boolean;
    send: boolean;
  };
  limits: {
    maxSearchResults: number;
    maxMessageBytes: number;
    maxAttachmentBytes: number;
    maxBodyChars: number;
    preparedEmailTtlSeconds: number;
    accessTokenTtlSeconds: number;
    refreshTokenTtlSeconds: number;
  };
  mailboxes: MailboxConfig[];
};

function decodeMasterSecret(value: string): Uint8Array {
  let decoded: Buffer;
  try {
    decoded = Buffer.from(value, "base64url");
  } catch {
    throw new Error("MCP_TOKEN_SECRET must be a base64url value");
  }
  if (decoded.length < 32) {
    throw new Error("MCP_TOKEN_SECRET must decode to at least 32 bytes");
  }
  return new Uint8Array(decoded);
}

function resolveFromCwd(value: string): string {
  return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
}

export function loadConfig(envInput: NodeJS.ProcessEnv = process.env): AppConfig {
  const env = EnvSchema.parse(envInput);
  const mailboxesPath = resolveFromCwd(env.MAILBOXES_CONFIG);
  const stateDir = resolveFromCwd(env.STATE_DIR);

  let rawMailboxes: unknown;
  try {
    rawMailboxes = JSON.parse(fs.readFileSync(mailboxesPath, "utf8"));
  } catch (error) {
    const reason = error instanceof Error ? error.message : String(error);
    throw new Error(`Unable to read mailbox configuration at ${mailboxesPath}: ${reason}`);
  }

  const mailboxFile = MailboxesFileSchema.parse(rawMailboxes);
  const mailboxes: MailboxConfig[] = mailboxFile.mailboxes.map((mailbox) => {
    const imapPassword = envInput[mailbox.imap.passwordEnv];
    if (!imapPassword) {
      throw new Error(
        `Missing environment variable ${mailbox.imap.passwordEnv} for mailbox ${mailbox.id}`,
      );
    }

    let smtpPassword: string | undefined;
    if (mailbox.smtp) {
      smtpPassword = envInput[mailbox.smtp.passwordEnv];
      if (!smtpPassword) {
        throw new Error(
          `Missing environment variable ${mailbox.smtp.passwordEnv} for mailbox ${mailbox.id}`,
        );
      }
    }

    return {
      ...mailbox,
      imapPassword,
      ...(smtpPassword ? { smtpPassword } : {}),
    };
  });

  const baseUrl = new URL(env.BASE_URL);
  const mcpUrl = new URL("/mcp", baseUrl);

  const staticClient = env.OAUTH_CLIENT_ID
    ? {
        clientId: env.OAUTH_CLIENT_ID,
        clientSecret: env.OAUTH_CLIENT_SECRET!,
        redirectUris: env.OAUTH_REDIRECT_URIS!.split(",")
          .map((value) => value.trim())
          .filter(Boolean),
      }
    : undefined;

  if (staticClient && staticClient.redirectUris.length === 0) {
    throw new Error("OAUTH_REDIRECT_URIS must contain at least one URI");
  }

  return {
    nodeEnv: env.NODE_ENV,
    host: env.HOST,
    port: env.PORT,
    baseUrl,
    mcpUrl,
    mailboxesPath,
    stateDir,
    profile: {
      id: env.MCP_PROFILE_ID,
      name: env.MCP_PROFILE_NAME,
      ...(env.MCP_PROFILE_EMAIL ? { email: env.MCP_PROFILE_EMAIL } : {}),
    },
    adminPasswordHash: env.MCP_ADMIN_PASSWORD_HASH,
    tokenSecret: decodeMasterSecret(env.MCP_TOKEN_SECRET),
    oauth: {
      allowDynamicRegistration: env.OAUTH_ALLOW_DYNAMIC_REGISTRATION,
      allowedRedirectHosts: env.OAUTH_ALLOWED_REDIRECT_HOSTS.split(",")
        .map((value) => value.trim().toLowerCase())
        .filter(Boolean),
      ...(staticClient ? { staticClient } : {}),
    },
    features: {
      drafts: env.ENABLE_DRAFTS,
      send: env.ENABLE_SEND,
    },
    limits: {
      maxSearchResults: env.MAX_SEARCH_RESULTS,
      maxMessageBytes: env.MAX_MESSAGE_BYTES,
      maxAttachmentBytes: env.MAX_ATTACHMENT_BYTES,
      maxBodyChars: env.MAX_BODY_CHARS,
      preparedEmailTtlSeconds: env.PREPARED_EMAIL_TTL_SECONDS,
      accessTokenTtlSeconds: env.ACCESS_TOKEN_TTL_SECONDS,
      refreshTokenTtlSeconds: env.REFRESH_TOKEN_TTL_SECONDS,
    },
    mailboxes,
  };
}
