const DEFAULT_MIME_TYPE = 'application/octet-stream';

const TEXT_ATTACHMENT_MIME_TYPES = {
  csv: 'text/csv',
  css: 'text/css',
  json: 'application/json',
  txt: 'text/plain',
  yaml: 'text/yaml'
} as const;

export const TEXT_LIKE_FILE_EXTENSIONS = [
  '.txt',
  '.text',
  '.log',
  '.md',
  '.markdown',
  '.csv',
  '.tsv',
  '.json',
  '.jsonl',
  '.ndjson',
  '.xml',
  '.yaml',
  '.yml',
  '.toml',
  '.ini',
  '.cfg',
  '.conf',
  '.env',
  '.sh',
  '.bash',
  '.zsh',
  '.js',
  '.jsx',
  '.ts',
  '.tsx',
  '.mts',
  '.cts',
  '.py',
  '.go',
  '.java',
  '.rb',
  '.php',
  '.sql',
  '.css',
  '.scss',
  '.html',
  '.htm'
] as const;

const MIME_TYPE_FILE_EXTENSIONS: Record<string, string> = {
  'application/json': '.json',
  'application/pdf': '.pdf',
  'application/x-yaml': '.yaml',
  'application/yaml': '.yaml',
  'image/gif': '.gif',
  'image/jpeg': '.jpg',
  'image/jpg': '.jpg',
  'image/png': '.png',
  'image/webp': '.webp',
  'text/css': '.css',
  'text/csv': '.csv',
  'text/plain': '.txt',
  'text/yaml': '.yaml'
};

type UploadAttachmentDataLike = {
  data: string;
  mimeType?: string | null;
  name?: string;
  fileName?: string;
};

type UploadImageAttachmentLike = {
  type: 'image';
  image: string;
  fileName: string;
};

type UploadPdfAttachmentLike = {
  type: 'pdf';
  data: string;
  fileName: string;
};

type UploadTextAttachmentLike = {
  type: 'csv' | 'css' | 'json' | 'txt' | 'yaml';
  content: string;
  fileName: string;
  mimeType?: string | null;
};

type UploadUnsupportedAttachmentLike = {
  type: 'uploaded';
};

export type AttachmentUploadSourceLike =
  | Blob
  | File
  | UploadAttachmentDataLike
  | UploadImageAttachmentLike
  | UploadPdfAttachmentLike
  | UploadTextAttachmentLike
  | UploadUnsupportedAttachmentLike;

export interface AttachmentUploadDescriptor {
  blob: Blob;
  fileName: string;
  mimeType: string;
}

export type AttachmentUploadScopeType = 'app' | 'org';
export type AttachmentUploadPurpose = 'attachment' | 'context';

export interface AppContextUploadDto {
  applicationId: string;
  branchName?: string;
  commitId?: string;
  purpose: 'context';
  scopeType: 'app';
  uploaded: true;
}

export interface AttachmentUploadDto {
  applicationId?: string;
  attachmentId: string;
  contentUrl: string;
  fileName: string;
  mimeType: string | null;
  purpose: AttachmentUploadPurpose;
  scopeType: AttachmentUploadScopeType;
  signedUrl: string;
  signedUrlExpiresAt: string;
  storageKey: string;
}

export type UploadBodyDto = AppContextUploadDto | AttachmentUploadDto;

// Backwards-compatible alias for existing attachment-only client consumers.
export type AttachmentUploadServerDto = AttachmentUploadDto;

export interface UploadedAttachmentLike {
  type: 'uploaded';
  url: string;
  signedUrl: string;
  mediaType: string;
  fileName: string;
  label?: string;
  insight?: string;
  storageKey?: string;
  signedUrlExpiresAt: string;
  scopeType?: AttachmentUploadScopeType;
  applicationId?: string;
}

export interface AttachmentUploadMetadata {
  label?: string;
  insight?: string;
}

export function buildAttachmentUploadMetadata(params: AttachmentUploadMetadata): AttachmentUploadMetadata | undefined {
  const label = normalizeNonEmptyString(params.label);
  const insight = normalizeNonEmptyString(params.insight);

  if (!label && !insight) {
    return undefined;
  }

  return {
    ...(label ? { label } : {}),
    ...(insight ? { insight } : {})
  };
}

function normalizeNonEmptyString(value: unknown): string | undefined {
  if (typeof value !== 'string') {
    return undefined;
  }

  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : undefined;
}

export function isTextLikeMimeType(mimeType: string): boolean {
  const normalizedMimeType = mimeType.toLowerCase();
  return (
    normalizedMimeType.startsWith('text/') ||
    normalizedMimeType.includes('json') ||
    normalizedMimeType.includes('xml') ||
    normalizedMimeType.includes('yaml') ||
    normalizedMimeType.includes('toml') ||
    normalizedMimeType.includes('javascript') ||
    normalizedMimeType.includes('typescript') ||
    normalizedMimeType.includes('ecmascript') ||
    normalizedMimeType.includes('markdown') ||
    normalizedMimeType.includes('shellscript') ||
    normalizedMimeType.includes('python') ||
    normalizedMimeType.includes('ruby') ||
    normalizedMimeType.includes('php') ||
    normalizedMimeType.includes('sql')
  );
}

export function getTextAttachmentType(params: {
  fileName?: string;
  mimeType?: string | null;
}): 'csv' | 'css' | 'json' | 'txt' | 'yaml' | null {
  const normalizedFileName = normalizeNonEmptyString(params.fileName)?.toLowerCase();
  const normalizedMimeType = normalizeNonEmptyString(params.mimeType)?.toLowerCase();

  if (normalizedMimeType === 'text/csv' || normalizedFileName?.endsWith('.csv')) {
    return 'csv';
  }
  if (normalizedMimeType === 'application/json' || normalizedFileName?.endsWith('.json')) {
    return 'json';
  }
  if (
    normalizedMimeType === 'text/yaml' ||
    normalizedMimeType === 'application/x-yaml' ||
    normalizedMimeType === 'application/yaml' ||
    normalizedFileName?.endsWith('.yaml') ||
    normalizedFileName?.endsWith('.yml')
  ) {
    return 'yaml';
  }
  if (normalizedMimeType === 'text/css' || normalizedFileName?.endsWith('.css')) {
    return 'css';
  }
  if (
    normalizedMimeType === 'text/plain' ||
    (normalizedMimeType ? isTextLikeMimeType(normalizedMimeType) : false) ||
    (normalizedFileName ? TEXT_LIKE_FILE_EXTENSIONS.some((extension) => normalizedFileName.endsWith(extension)) : false)
  ) {
    return 'txt';
  }

  return null;
}

function getTextLikeMimeTypeFromFileName(fileName: string | undefined): string | undefined {
  const textAttachmentType = getTextAttachmentType({ fileName });
  return textAttachmentType ? TEXT_ATTACHMENT_MIME_TYPES[textAttachmentType] : undefined;
}

export function normalizeAttachmentMimeType(params: {
  fileName?: string;
  mimeType?: string | null;
  fallbackMimeType?: string | null;
}): string {
  const normalizedMimeType =
    normalizeNonEmptyString(params.mimeType)?.toLowerCase() ?? normalizeNonEmptyString(params.fallbackMimeType)?.toLowerCase();
  const textLikeMimeType = getTextLikeMimeTypeFromFileName(params.fileName);

  if (!textLikeMimeType) {
    return normalizedMimeType ?? DEFAULT_MIME_TYPE;
  }

  if (!normalizedMimeType) {
    return textLikeMimeType;
  }

  return isTextLikeMimeType(normalizedMimeType) ? normalizedMimeType : textLikeMimeType;
}

function getDefaultFileName(mimeType: string): string {
  const normalizedMimeType = mimeType.toLowerCase();
  const knownExtension = MIME_TYPE_FILE_EXTENSIONS[normalizedMimeType];
  if (knownExtension) {
    return `attachment${knownExtension}`;
  }

  if (normalizedMimeType.startsWith('image/')) {
    const subtype = normalizedMimeType.slice('image/'.length).split(/[+;]/)[0]?.trim();
    if (subtype) {
      return `attachment.${subtype}`;
    }
  }

  return 'attachment';
}

function resolveFileName(params: { fileName: unknown; fallbackFileName?: string; mimeType: string }): string {
  return (
    normalizeNonEmptyString(params.fileName) ?? normalizeNonEmptyString(params.fallbackFileName) ?? getDefaultFileName(params.mimeType)
  );
}

function decodeBase64(base64Data: string): Uint8Array {
  return Uint8Array.from(atob(base64Data), (char) => char.charCodeAt(0));
}

function parseDataUrl(data: string): { base64Data: string; mimeType?: string } | null {
  const match = data.match(/^data:([^;,]+)?(?:;base64)?,(.*)$/s);
  if (!match) {
    return null;
  }

  return {
    mimeType: normalizeNonEmptyString(match[1]),
    base64Data: match[2] ?? ''
  };
}

function isAttachmentDataLike(source: AttachmentUploadSourceLike): source is UploadAttachmentDataLike {
  return 'data' in source && typeof source.data === 'string' && !('type' in source);
}

export function toAttachmentUploadDescriptor(
  source: AttachmentUploadSourceLike,
  options?: {
    fallbackFileName?: string;
    fallbackMimeType?: string;
  }
): AttachmentUploadDescriptor | null {
  if (source instanceof File) {
    const mimeType = normalizeAttachmentMimeType({
      fileName: source.name,
      mimeType: source.type,
      fallbackMimeType: options?.fallbackMimeType
    });

    return {
      blob: source,
      fileName: resolveFileName({
        fileName: source.name,
        fallbackFileName: options?.fallbackFileName,
        mimeType
      }),
      mimeType
    };
  }

  if (source instanceof Blob) {
    const sourceWithOptionalName = source as Blob & { name?: unknown };
    const mimeType = normalizeAttachmentMimeType({
      fileName: typeof sourceWithOptionalName.name === 'string' ? sourceWithOptionalName.name : options?.fallbackFileName,
      mimeType: source.type,
      fallbackMimeType: options?.fallbackMimeType
    });

    return {
      blob: source,
      fileName: resolveFileName({
        fileName: sourceWithOptionalName.name,
        fallbackFileName: options?.fallbackFileName,
        mimeType
      }),
      mimeType
    };
  }

  if (isAttachmentDataLike(source)) {
    const parsedDataUrl = parseDataUrl(source.data);
    const mimeType = normalizeAttachmentMimeType({
      fileName: typeof source.name === 'string' ? source.name : source.fileName,
      mimeType: parsedDataUrl?.mimeType ?? source.mimeType,
      fallbackMimeType: options?.fallbackMimeType
    });
    const base64Data = parsedDataUrl?.base64Data ?? source.data;

    return {
      blob: new Blob([decodeBase64(base64Data)], { type: mimeType }),
      fileName: resolveFileName({
        fileName: source.name ?? source.fileName,
        fallbackFileName: options?.fallbackFileName,
        mimeType
      }),
      mimeType
    };
  }

  if (source.type === 'image') {
    const parsedDataUrl = parseDataUrl(source.image);
    if (!parsedDataUrl?.mimeType) {
      return null;
    }

    return {
      blob: new Blob([decodeBase64(parsedDataUrl.base64Data)], {
        type: parsedDataUrl.mimeType
      }),
      fileName: resolveFileName({
        fileName: source.fileName,
        fallbackFileName: options?.fallbackFileName,
        mimeType: parsedDataUrl.mimeType
      }),
      mimeType: parsedDataUrl.mimeType
    };
  }

  if (source.type === 'pdf') {
    const parsedDataUrl = parseDataUrl(source.data);
    const mimeType = parsedDataUrl?.mimeType ?? normalizeNonEmptyString(options?.fallbackMimeType) ?? 'application/pdf';

    return {
      blob: new Blob([decodeBase64(parsedDataUrl?.base64Data ?? source.data)], {
        type: mimeType
      }),
      fileName: resolveFileName({
        fileName: source.fileName,
        fallbackFileName: options?.fallbackFileName,
        mimeType
      }),
      mimeType
    };
  }

  if (source.type === 'csv' || source.type === 'css' || source.type === 'json' || source.type === 'txt' || source.type === 'yaml') {
    const mimeType = normalizeAttachmentMimeType({
      fileName: source.fileName,
      mimeType: source.mimeType,
      fallbackMimeType: options?.fallbackMimeType ?? TEXT_ATTACHMENT_MIME_TYPES[source.type]
    });

    return {
      blob: new Blob([source.content], { type: mimeType }),
      fileName: resolveFileName({
        fileName: source.fileName,
        fallbackFileName: options?.fallbackFileName,
        mimeType
      }),
      mimeType
    };
  }

  return null;
}

export function buildAttachmentUploadFormData(params: {
  descriptor: AttachmentUploadDescriptor;
  prefix: string;
  fieldName?: string;
  branchId?: string;
  commitId?: string;
  metadata?: AttachmentUploadMetadata;
}): FormData {
  const formData = new FormData();
  formData.append(params.fieldName ?? 'file', params.descriptor.blob, params.descriptor.fileName);
  formData.append('prefix', params.prefix);

  if (params.branchId) {
    formData.append('branchId', params.branchId);
  }

  if (params.commitId) {
    formData.append('commitId', params.commitId);
  }

  const metadata = buildAttachmentUploadMetadata(params.metadata ?? {});
  if (metadata) {
    formData.append('metadata', JSON.stringify(metadata));
  }

  return formData;
}

export function createUploadedAttachmentFromServer(params: {
  response: AttachmentUploadServerDto;
  fallbackFileName: string;
  fallbackMimeType: string;
  label?: string;
  insight?: string;
}): UploadedAttachmentLike {
  const mediaType = normalizeNonEmptyString(params.response.mimeType) ?? params.fallbackMimeType;

  return {
    type: 'uploaded',
    url: normalizeNonEmptyString(params.response.contentUrl) ?? params.response.signedUrl,
    signedUrl: params.response.signedUrl,
    mediaType,
    fileName: normalizeNonEmptyString(params.response.fileName) ?? params.fallbackFileName,
    label: params.label,
    insight: params.insight,
    storageKey: normalizeNonEmptyString(params.response.storageKey),
    signedUrlExpiresAt: params.response.signedUrlExpiresAt,
    scopeType: params.response.scopeType,
    applicationId: normalizeNonEmptyString(params.response.applicationId)
  };
}

/**
 * Shared transport for POST /api/v1/files/upload. Builds form-data, URL with query params,
 * fetches, and returns the raw server DTO. Callers map to their own types (e.g. Attachment,
 * AttachmentUploadResult).
 */
export async function uploadAttachmentToServerApi(params: {
  baseUrl: string;
  authorization: string;
  descriptor: AttachmentUploadDescriptor;
  scopeType: AttachmentUploadScopeType;
  purpose: AttachmentUploadPurpose;
  applicationId?: string;
  branchName?: string;
  commitId?: string;
  prefix?: string;
  metadata?: AttachmentUploadMetadata;
}): Promise<AttachmentUploadServerDto> {
  const {
    baseUrl,
    authorization,
    descriptor,
    scopeType,
    purpose,
    applicationId,
    branchName,
    commitId,
    prefix = 'message-attachment',
    metadata
  } = params;

  const formData = buildAttachmentUploadFormData({
    descriptor,
    prefix,
    metadata,
    ...(branchName != null && { branchId: branchName }),
    ...(commitId != null && { commitId })
  });

  const uploadUrl = new URL('/api/v1/files/upload', baseUrl);
  uploadUrl.searchParams.set('scopeType', scopeType);
  uploadUrl.searchParams.set('purpose', purpose);
  if (applicationId != null && applicationId.trim() !== '') {
    uploadUrl.searchParams.set('applicationId', applicationId);
  }
  if (branchName != null && branchName.trim() !== '') {
    uploadUrl.searchParams.set('branchName', branchName);
  }
  if (commitId != null && commitId.trim() !== '') {
    uploadUrl.searchParams.set('commitId', commitId);
  }

  const response = await fetch(uploadUrl.toString(), {
    method: 'POST',
    body: formData,
    headers: {
      Authorization: authorization
    }
  });

  if (!response.ok) {
    const errorText = await response.text().catch(() => '');
    throw new Error(`Attachment upload failed: status=${response.status}${errorText ? `, body=${errorText}` : ''}`);
  }

  return (await response.json()) as AttachmentUploadServerDto;
}
