import fetch from "node-fetch";
import {
  DescribeLogGroupsCommand,
  DescribeLogStreamsCommand,
  FilterLogEventsCommand
} from "@aws-sdk/client-cloudwatch-logs";
import { GetSecretValueCommand, ListSecretsCommand } from "@aws-sdk/client-secrets-manager";
import { GetObjectCommand, HeadObjectCommand, ListBucketsCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
import { simpleParser } from "mailparser";
import {
  CLOUDWATCH_ENDPOINT,
  CLOUDWATCH_REGION,
  FETCH_TIMEOUT_MS,
  S3_ENDPOINT,
  S3_IMAGE_PREVIEW_MAX_BYTES,
  S3_PREVIEW_MAX_BYTES,
  S3_REGION,
  SES_ENDPOINT,
  SECRETS_ENDPOINT,
  SECRETS_REGION,
  cloudWatchLogsClient,
  s3Client,
  secretsManagerClient
} from "./config.js";

async function loadMessages() {
  const startedAt = Date.now();
  const sesMessages = await fetchSesMessages();
  const messages = await Promise.all(sesMessages.map((message, index) => toMessageViewModel(message, index)));

  messages.sort((left, right) => {
    if ((right.timestampMs || 0) !== (left.timestampMs || 0)) {
      return (right.timestampMs || 0) - (left.timestampMs || 0);
    }

    return right.index - left.index;
  });

  return {
    endpoint: SES_ENDPOINT,
    fetchedAt: new Date().toISOString(),
    fetchDurationMs: Date.now() - startedAt,
    totalMessages: messages.length,
    parseErrors: messages.filter((message) => Boolean(message.parseError)).length,
    latestMessageTimestamp: messages[0]?.timestamp || "",
    messages
  };
}

async function fetchSesMessages() {
  const response = await fetch(SES_ENDPOINT, {
    signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
  });

  if (!response.ok) {
    throw new Error(`SES endpoint responded with ${response.status}`);
  }

  const data = await response.json();
  return Array.isArray(data.messages) ? data.messages : [];
}

async function loadLogGroups() {
  const groups = [];
  let nextToken;
  let pageCount = 0;

  do {
    const response = await cloudWatchLogsClient.send(
      new DescribeLogGroupsCommand({
        nextToken,
        limit: 50
      })
    );

    groups.push(
      ...(response.logGroups || []).map((group) => ({
        name: group.logGroupName || "",
        arn: group.arn || "",
        storedBytes: group.storedBytes || 0,
        retentionInDays: group.retentionInDays || 0,
        creationTime: group.creationTime || 0
      }))
    );

    nextToken = response.nextToken;
    pageCount += 1;
  } while (nextToken && pageCount < 10);

  return groups.sort((left, right) => left.name.localeCompare(right.name));
}

async function loadLogStreams(logGroupName) {
  const streams = [];
  let nextToken;
  let pageCount = 0;

  do {
    const response = await cloudWatchLogsClient.send(
      new DescribeLogStreamsCommand({
        logGroupName,
        descending: true,
        orderBy: "LastEventTime",
        nextToken,
        limit: 50
      })
    );

    streams.push(
      ...(response.logStreams || []).map((stream) => ({
        name: stream.logStreamName || "",
        arn: stream.arn || "",
        lastEventTimestamp: stream.lastEventTimestamp || 0,
        lastIngestionTime: stream.lastIngestionTime || 0,
        storedBytes: stream.storedBytes || 0
      }))
    );

    nextToken = response.nextToken;
    pageCount += 1;
  } while (nextToken && pageCount < 6 && streams.length < 250);

  return streams;
}

async function loadLogEvents({ logGroupName, logStreamName, windowMs, limit }) {
  const startedAt = Date.now();
  const eventMap = new Map();
  const startTime = Date.now() - windowMs;
  let nextToken;
  let previousToken = "";
  let pageCount = 0;
  let searchedLogStreams = 0;

  do {
    const response = await cloudWatchLogsClient.send(
      new FilterLogEventsCommand({
        logGroupName,
        logStreamNames: logStreamName ? [logStreamName] : undefined,
        startTime,
        endTime: Date.now(),
        limit,
        nextToken
      })
    );

    for (const event of response.events || []) {
      const id =
        event.eventId || `${event.logStreamName || "stream"}-${event.timestamp || 0}-${event.ingestionTime || 0}`;

      if (!eventMap.has(id)) {
        const message = String(event.message || "").trim();
        eventMap.set(id, {
          id,
          timestamp: event.timestamp || 0,
          ingestionTime: event.ingestionTime || 0,
          logStreamName: event.logStreamName || "",
          message,
          preview: buildLogPreview(message)
        });
      }
    }

    searchedLogStreams = Math.max(searchedLogStreams, (response.searchedLogStreams || []).length);
    previousToken = nextToken || "";
    nextToken = response.nextToken;
    pageCount += 1;
  } while (nextToken && nextToken !== previousToken && pageCount < 10 && eventMap.size < limit);

  const events = [...eventMap.values()]
    .sort((left, right) => {
      if ((right.timestamp || 0) !== (left.timestamp || 0)) {
        return (right.timestamp || 0) - (left.timestamp || 0);
      }

      return left.logStreamName.localeCompare(right.logStreamName);
    })
    .slice(0, limit);

  return {
    endpoint: CLOUDWATCH_ENDPOINT,
    region: CLOUDWATCH_REGION,
    logGroupName,
    logStreamName,
    fetchDurationMs: Date.now() - startedAt,
    latestTimestamp: events[0]?.timestamp || 0,
    searchedLogStreams,
    totalEvents: events.length,
    events
  };
}

async function loadSecrets() {
  const startedAt = Date.now();
  const secrets = [];
  let nextToken;
  let pageCount = 0;

  do {
    const response = await secretsManagerClient.send(
      new ListSecretsCommand({
        NextToken: nextToken,
        MaxResults: 50
      })
    );

    secrets.push(
      ...(response.SecretList || []).map((secret, index) => ({
        id: secret.ARN || secret.Name || `secret-${index}`,
        name: secret.Name || "Unnamed secret",
        arn: secret.ARN || "",
        description: secret.Description || "",
        createdDate: normalizeTimestamp(secret.CreatedDate),
        lastChangedDate: normalizeTimestamp(secret.LastChangedDate),
        lastAccessedDate: normalizeTimestamp(secret.LastAccessedDate),
        deletedDate: normalizeTimestamp(secret.DeletedDate),
        primaryRegion: secret.PrimaryRegion || "",
        owningService: secret.OwningService || "",
        rotationEnabled: Boolean(secret.RotationEnabled),
        versionCount: Object.keys(secret.SecretVersionsToStages || {}).length,
        tagCount: Array.isArray(secret.Tags) ? secret.Tags.length : 0,
        tags: (secret.Tags || [])
          .map((tag) => ({
            key: tag.Key || "",
            value: tag.Value || ""
          }))
          .filter((tag) => tag.key || tag.value)
      }))
    );

    nextToken = response.NextToken;
    pageCount += 1;
  } while (nextToken && pageCount < 10 && secrets.length < 500);

  secrets.sort((left, right) => {
    const leftTime = Date.parse(left.lastChangedDate || left.createdDate || 0) || 0;
    const rightTime = Date.parse(right.lastChangedDate || right.createdDate || 0) || 0;

    if (rightTime !== leftTime) {
      return rightTime - leftTime;
    }

    return left.name.localeCompare(right.name);
  });

  return {
    endpoint: SECRETS_ENDPOINT,
    region: SECRETS_REGION,
    fetchedAt: new Date().toISOString(),
    fetchDurationMs: Date.now() - startedAt,
    totalSecrets: secrets.length,
    latestTimestamp: secrets[0]?.lastChangedDate || secrets[0]?.createdDate || "",
    secrets
  };
}

async function loadSecretValue(secretId) {
  const startedAt = Date.now();
  const response = await secretsManagerClient.send(
    new GetSecretValueCommand({
      SecretId: secretId
    })
  );

  const secretBinary = response.SecretBinary
    ? typeof response.SecretBinary === "string"
      ? response.SecretBinary
      : Buffer.from(response.SecretBinary).toString("base64")
    : "";

  return {
    endpoint: SECRETS_ENDPOINT,
    region: SECRETS_REGION,
    fetchDurationMs: Date.now() - startedAt,
    id: secretId,
    name: response.Name || "",
    arn: response.ARN || "",
    versionId: response.VersionId || "",
    versionStages: Array.isArray(response.VersionStages) ? response.VersionStages : [],
    createdDate: normalizeTimestamp(response.CreatedDate),
    secretString: typeof response.SecretString === "string" ? response.SecretString : "",
    secretBinary
  };
}

async function loadS3Buckets() {
  const startedAt = Date.now();
  const response = await s3Client.send(new ListBucketsCommand({}));
  const buckets = (response.Buckets || [])
    .map((bucket) => ({
      name: bucket.Name || "",
      creationDate: normalizeTimestamp(bucket.CreationDate)
    }))
    .filter((bucket) => bucket.name)
    .sort((left, right) => left.name.localeCompare(right.name));

  return {
    endpoint: S3_ENDPOINT,
    region: S3_REGION,
    fetchedAt: new Date().toISOString(),
    fetchDurationMs: Date.now() - startedAt,
    totalBuckets: buckets.length,
    buckets
  };
}

async function loadS3Objects({ bucket, prefix }) {
  const startedAt = Date.now();
  const objects = [];
  let continuationToken;
  let pageCount = 0;

  do {
    const response = await s3Client.send(
      new ListObjectsV2Command({
        Bucket: bucket,
        Prefix: prefix || undefined,
        ContinuationToken: continuationToken,
        MaxKeys: 200
      })
    );

    objects.push(
      ...(response.Contents || []).map((object, index) => ({
        id: `${bucket}::${object.Key || index}`,
        bucket,
        key: object.Key || "",
        size: object.Size || 0,
        lastModified: normalizeTimestamp(object.LastModified),
        etag: String(object.ETag || "").replace(/^"|"$/g, ""),
        storageClass: object.StorageClass || "STANDARD"
      }))
    );

    continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
    pageCount += 1;
  } while (continuationToken && pageCount < 10 && objects.length < 1000);

  objects.sort((left, right) => {
    const leftTime = Date.parse(left.lastModified || 0) || 0;
    const rightTime = Date.parse(right.lastModified || 0) || 0;

    if (rightTime !== leftTime) {
      return rightTime - leftTime;
    }

    return left.key.localeCompare(right.key);
  });

  return {
    endpoint: S3_ENDPOINT,
    region: S3_REGION,
    bucket,
    prefix,
    fetchedAt: new Date().toISOString(),
    fetchDurationMs: Date.now() - startedAt,
    totalObjects: objects.length,
    latestTimestamp: objects[0]?.lastModified || "",
    objects
  };
}

async function loadS3ObjectPreview({ bucket, key }) {
  const startedAt = Date.now();
  const head = await s3Client.send(
    new HeadObjectCommand({
      Bucket: bucket,
      Key: key
    })
  );

  const contentType = head.ContentType || guessObjectContentType(key);
  const contentLength = Number(head.ContentLength || 0);
  const previewType = resolveS3PreviewType(contentType, key);
  const result = {
    endpoint: S3_ENDPOINT,
    region: S3_REGION,
    bucket,
    key,
    fetchDurationMs: 0,
    contentType,
    contentLength,
    etag: String(head.ETag || "").replace(/^"|"$/g, ""),
    lastModified: normalizeTimestamp(head.LastModified),
    metadata: head.Metadata || {},
    previewType,
    previewText: "",
    imageDataUrl: "",
    truncated: false
  };

  const shouldLoadTextPreview = previewType === "json" || previewType === "text" || previewType === "html";
  const shouldLoadImagePreview =
    previewType === "image" && contentLength > 0 && contentLength <= S3_IMAGE_PREVIEW_MAX_BYTES;

  if ((shouldLoadTextPreview || shouldLoadImagePreview) && contentLength > 0) {
    const previewBytes = Math.max(1, Math.min(contentLength || S3_PREVIEW_MAX_BYTES, S3_PREVIEW_MAX_BYTES));
    const response = await s3Client.send(
      new GetObjectCommand({
        Bucket: bucket,
        Key: key,
        Range: `bytes=0-${previewBytes - 1}`
      })
    );
    const content = Buffer.from(await response.Body.transformToByteArray());
    result.truncated = contentLength > content.length;

    if (shouldLoadImagePreview) {
      result.imageDataUrl = `data:${contentType};base64,${content.toString("base64")}`;
    } else {
      result.previewText = content.toString("utf8");
    }
  }

  result.fetchDurationMs = Date.now() - startedAt;
  return result;
}

async function loadServiceHealthSummary() {
  const startedAt = Date.now();
  const [sesResult, logsResult, secretsResult, s3Result] = await Promise.allSettled([
    fetchSesMessages(),
    loadLogGroups(),
    loadSecrets(),
    loadS3Buckets()
  ]);

  return {
    fetchedAt: new Date().toISOString(),
    fetchDurationMs: Date.now() - startedAt,
    services: {
      emails: summarizeHealthResult({
        icon: "✉️",
        panel: "emails",
        label: "SES Emails",
        result: sesResult,
        count: sesResult.status === "fulfilled" ? sesResult.value.length : 0,
        detail: SES_ENDPOINT,
        noun: "email"
      }),
      logs: summarizeHealthResult({
        icon: "📜",
        panel: "logs",
        label: "CloudWatch Logs",
        result: logsResult,
        count: logsResult.status === "fulfilled" ? logsResult.value.length : 0,
        detail: `${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`,
        noun: "group"
      }),
      secrets: summarizeHealthResult({
        icon: "🔐",
        panel: "secrets",
        label: "Secrets Manager",
        result: secretsResult,
        count: secretsResult.status === "fulfilled" ? secretsResult.value.totalSecrets : 0,
        detail: `${SECRETS_ENDPOINT} (${SECRETS_REGION})`,
        noun: "secret"
      }),
      s3: summarizeHealthResult({
        icon: "🪣",
        panel: "s3",
        label: "S3 Explorer",
        result: s3Result,
        count: s3Result.status === "fulfilled" ? s3Result.value.totalBuckets : 0,
        detail: `${S3_ENDPOINT} (${S3_REGION})`,
        noun: "bucket"
      })
    }
  };
}

async function findSesMessageById(id) {
  const messages = await fetchSesMessages();
  return messages.find((message, index) => resolveMessageId(message, index) === id) || null;
}

async function parseSesMessageById(id) {
  const message = await findSesMessageById(id);

  if (!message) {
    return null;
  }

  return simpleParser(message.RawData || "");
}

async function toMessageViewModel(message, index) {
  const id = resolveMessageId(message, index);

  try {
    const parsed = await simpleParser(message.RawData || "");
    const textContent = normalizeText(parsed.text || "");
    const renderedHtml = buildRenderedHtml(parsed.html || parsed.textAsHtml || "");
    const timestamp = normalizeTimestamp(message.Timestamp || parsed.date);

    return {
      id,
      index,
      from: formatAddressList(parsed.from) || message.Source || "Unknown sender",
      to: formatAddressList(parsed.to) || "No To Address",
      replyTo: formatAddressList(parsed.replyTo),
      subject: parsed.subject || "No Subject",
      region: message.Region || "",
      timestamp,
      timestampMs: timestamp ? Date.parse(timestamp) : 0,
      messageId: parsed.messageId || "",
      rawSizeBytes: Buffer.byteLength(message.RawData || "", "utf8"),
      attachmentCount: parsed.attachments.length,
      attachments: parsed.attachments.map((attachment, attachmentIndex) => ({
        index: attachmentIndex,
        filename: resolveAttachmentFilename(attachment, attachmentIndex),
        contentType: attachment.contentType || "application/octet-stream",
        size: attachment.size || 0
      })),
      preview: buildPreview(textContent, renderedHtml),
      textContent,
      renderedHtml,
      hasHtml: Boolean(renderedHtml),
      parseError: ""
    };
  } catch (error) {
    return {
      id,
      index,
      from: message.Source || "Unknown sender",
      to: "Unknown recipient",
      replyTo: "",
      subject: "Unable to parse message",
      region: message.Region || "",
      timestamp: normalizeTimestamp(message.Timestamp),
      timestampMs: message.Timestamp ? Date.parse(message.Timestamp) : 0,
      messageId: "",
      rawSizeBytes: Buffer.byteLength(message.RawData || "", "utf8"),
      attachmentCount: 0,
      attachments: [],
      preview: "This message could not be parsed. Open the raw view to inspect the MIME source.",
      textContent: "",
      renderedHtml: "",
      hasHtml: false,
      parseError: error.message
    };
  }
}

function resolveMessageId(message, index = 0) {
  return message.Id || `${message.Timestamp || "unknown"}-${message.Source || "unknown"}-${index}`;
}

function resolveAttachmentFilename(attachment, index = 0) {
  if (attachment?.filename) {
    return attachment.filename;
  }

  return `attachment-${index + 1}${attachmentExtension(attachment?.contentType)}`;
}

function attachmentExtension(contentType) {
  const normalized = String(contentType || "")
    .split(";")[0]
    .trim()
    .toLowerCase();

  return (
    {
      "application/json": ".json",
      "application/pdf": ".pdf",
      "application/zip": ".zip",
      "image/gif": ".gif",
      "image/jpeg": ".jpg",
      "image/png": ".png",
      "image/webp": ".webp",
      "text/calendar": ".ics",
      "text/csv": ".csv",
      "text/html": ".html",
      "text/plain": ".txt"
    }[normalized] || ""
  );
}

function buildAttachmentDisposition(filename) {
  const fallback = String(filename || "attachment")
    .replace(/[^\x20-\x7e]/g, "_")
    .replace(/["\\]/g, "_");

  return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename || "attachment")}`;
}

function buildInlineDisposition(filename) {
  const fallback = String(filename || "file")
    .replace(/[^\x20-\x7e]/g, "_")
    .replace(/["\\]/g, "_");

  return `inline; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename || "file")}`;
}

function basenameFromKey(key) {
  const value = String(key || "");
  const parts = value.split("/").filter(Boolean);
  return parts[parts.length - 1] || "file";
}

function guessObjectContentType(key) {
  const normalizedKey = String(key || "").toLowerCase();

  if (normalizedKey.endsWith(".json")) {
    return "application/json";
  }

  if (normalizedKey.endsWith(".csv")) {
    return "text/csv";
  }

  if (normalizedKey.endsWith(".html") || normalizedKey.endsWith(".htm")) {
    return "text/html";
  }

  if (normalizedKey.endsWith(".txt") || normalizedKey.endsWith(".log") || normalizedKey.endsWith(".md")) {
    return "text/plain";
  }

  if (normalizedKey.endsWith(".png")) {
    return "image/png";
  }

  if (normalizedKey.endsWith(".jpg") || normalizedKey.endsWith(".jpeg")) {
    return "image/jpeg";
  }

  if (normalizedKey.endsWith(".gif")) {
    return "image/gif";
  }

  if (normalizedKey.endsWith(".webp")) {
    return "image/webp";
  }

  if (normalizedKey.endsWith(".svg")) {
    return "image/svg+xml";
  }

  if (normalizedKey.endsWith(".pdf")) {
    return "application/pdf";
  }

  return "application/octet-stream";
}

function resolveS3PreviewType(contentType, key) {
  const normalizedType = String(contentType || "").toLowerCase();
  const normalizedKey = String(key || "").toLowerCase();

  if (normalizedType.includes("json") || normalizedKey.endsWith(".json")) {
    return "json";
  }

  if (normalizedType.startsWith("image/")) {
    return "image";
  }

  if (normalizedType.includes("html") || normalizedKey.endsWith(".html") || normalizedKey.endsWith(".htm")) {
    return "html";
  }

  if (
    normalizedType.startsWith("text/") ||
    [".txt", ".log", ".csv", ".xml", ".yml", ".yaml", ".md"].some((extension) => normalizedKey.endsWith(extension))
  ) {
    return "text";
  }

  return "binary";
}

function summarizeHealthResult({ icon, panel, label, result, count, detail, noun }) {
  if (result.status === "fulfilled") {
    return {
      ok: true,
      icon,
      panel,
      label,
      count,
      summary: `${count} ${noun}${count === 1 ? "" : "s"}`,
      detail
    };
  }

  return {
    ok: false,
    icon,
    panel,
    label,
    count: 0,
    summary: "Needs attention",
    detail: result.reason?.message || detail
  };
}

function normalizeTimestamp(value) {
  if (!value) {
    return "";
  }

  const date = value instanceof Date ? value : new Date(value);
  return Number.isNaN(date.getTime()) ? "" : date.toISOString();
}

function normalizeText(value) {
  return String(value || "")
    .replace(/\r\n/g, "\n")
    .trim();
}

function buildPreview(textContent, renderedHtml) {
  const source = (textContent || stripTags(renderedHtml)).replace(/\s+/g, " ").trim();

  if (!source) {
    return "No message preview available.";
  }

  return source.length > 220 ? `${source.slice(0, 217)}...` : source;
}

function buildLogPreview(message) {
  const source = String(message || "")
    .replace(/\s+/g, " ")
    .trim();

  if (!source) {
    return "No log preview available.";
  }

  return source.length > 220 ? `${source.slice(0, 217)}...` : source;
}

function clampNumber(value, fallback, min, max) {
  const parsed = Number(value);

  if (!Number.isFinite(parsed)) {
    return fallback;
  }

  return Math.min(Math.max(parsed, min), max);
}

function buildRenderedHtml(html) {
  if (!html) {
    return "";
  }

  const value = String(html);
  const hasDocument = /<html[\s>]/i.test(value) || /<!doctype/i.test(value);

  if (hasDocument) {
    return value;
  }

  return `<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <base target="_blank">
    <style>body{margin:0;padding:16px;font-family:Arial,sans-serif;background:#fff;}</style>
  </head>
  <body>${value}</body>
</html>`;
}

function stripTags(value) {
  return String(value || "")
    .replace(/<style[\s\S]*?<\/style>/gi, " ")
    .replace(/<script[\s\S]*?<\/script>/gi, " ")
    .replace(/<[^>]+>/g, " ");
}

function formatAddressList(addresses) {
  if (!addresses?.value?.length) {
    return "";
  }

  return addresses.value
    .map(({ name, address }) => {
      if (name && address) {
        return `${name} <${address}>`;
      }

      return address || name || "";
    })
    .filter(Boolean)
    .join(", ");
}

async function loadMessageAttachment(messageId, attachmentIndex) {
  const parsed = await parseSesMessageById(messageId);

  if (!parsed) {
    return null;
  }

  const attachment = parsed.attachments?.[attachmentIndex];

  if (!attachment) {
    return null;
  }

  return {
    filename: resolveAttachmentFilename(attachment, attachmentIndex),
    contentType: attachment.contentType || "application/octet-stream",
    content: Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content || "")
  };
}

async function loadS3ObjectDownload({ bucket, key }) {
  const response = await s3Client.send(
    new GetObjectCommand({
      Bucket: bucket,
      Key: key
    })
  );

  return {
    filename: basenameFromKey(key),
    contentType: response.ContentType || guessObjectContentType(key),
    content: Buffer.from(await response.Body.transformToByteArray())
  };
}

export {
  buildAttachmentDisposition,
  buildInlineDisposition,
  clampNumber,
  findSesMessageById,
  loadLogEvents,
  loadLogGroups,
  loadLogStreams,
  loadMessageAttachment,
  loadMessages,
  loadS3Buckets,
  loadS3ObjectDownload,
  loadS3ObjectPreview,
  loadS3Objects,
  loadSecretValue,
  loadSecrets,
  loadServiceHealthSummary
};
