import {
  OAuth2PropertyValue,
  PieceAuthProperty,
  Property,
  StaticDropdownProperty,
  createAction,
  StaticPropsValue,
  InputPropertyMap,
  FilesService,
  AppConnectionValueForAuthProperty,
  ExtractPieceAuthPropertyTypeForMethods,
  ApFile,
} from '@activepieces/pieces-framework';
import {
  HttpError,
  HttpHeaders,
  HttpMethod,
  HttpRequest,
  QueryParams,
  httpClient,
} from '../http';
import { assertNotNullOrUndefined, isEmpty, isNil } from '@activepieces/core-utils';
import fs from 'fs';
// Self-contained content-type → file-extension lookup, replacing the heavy
// `mime-types`/`mime-db` dependency (~134 KB inlined into every piece bundle).
// Covers the common types returned for binary HTTP responses; unknown → ''.
const CONTENT_TYPE_EXTENSIONS: Record<string, string> = {
  'application/json': 'json',
  'application/pdf': 'pdf',
  'application/xml': 'xml',
  'text/xml': 'xml',
  'application/zip': 'zip',
  'application/gzip': 'gz',
  'application/x-tar': 'tar',
  'application/octet-stream': 'bin',
  'application/msword': 'doc',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
  'application/vnd.ms-excel': 'xls',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
  'application/vnd.ms-powerpoint': 'ppt',
  'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
  'application/rtf': 'rtf',
  'application/javascript': 'js',
  'application/x-www-form-urlencoded': 'bin',
  'text/plain': 'txt',
  'text/html': 'html',
  'text/css': 'css',
  'text/csv': 'csv',
  'text/calendar': 'ics',
  'text/markdown': 'md',
  'image/png': 'png',
  'image/jpeg': 'jpeg',
  'image/jpg': 'jpg',
  'image/gif': 'gif',
  'image/webp': 'webp',
  'image/svg+xml': 'svg',
  'image/bmp': 'bmp',
  'image/tiff': 'tiff',
  'image/x-icon': 'ico',
  'image/vnd.microsoft.icon': 'ico',
  'image/heic': 'heic',
  'audio/mpeg': 'mp3',
  'audio/mp4': 'm4a',
  'audio/wav': 'wav',
  'audio/x-wav': 'wav',
  'audio/ogg': 'ogg',
  'audio/webm': 'weba',
  'video/mp4': 'mp4',
  'video/mpeg': 'mpeg',
  'video/webm': 'webm',
  'video/quicktime': 'mov',
  'video/x-msvideo': 'avi',
  'font/woff': 'woff',
  'font/woff2': 'woff2',
  'font/ttf': 'ttf',
  'font/otf': 'otf',
};

function contentTypeToExtension(contentType: string): string {
  const type = contentType.split(';')[0].trim().toLowerCase();
  return CONTENT_TYPE_EXTENSIONS[type] ?? '';
}
import FormData from 'form-data';

export const getAccessTokenOrThrow = (
  auth: OAuth2PropertyValue | undefined
): string => {
  const accessToken = auth?.access_token;

  if (accessToken === undefined) {
    throw new Error('Invalid bearer token');
  }

  return accessToken;
};
const joinBaseUrlWithRelativePath = ({
  baseUrl,
  relativePath,
}: {
  baseUrl: string;
  relativePath: string;
}) => {
  const baseUrlWithSlash = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
  const relativePathWithoutSlash = relativePath.startsWith('/')
    ? relativePath.slice(1)
    : relativePath;
  return `${baseUrlWithSlash}${relativePathWithoutSlash}`;
};

const getBaseUrlForDescription = <
  PieceAuth extends PieceAuthProperty | PieceAuthProperty[] | undefined
>(
  baseUrl: BaseUrlGetter<PieceAuth>,
  auth?: AppConnectionValueForAuthProperty<
    ExtractPieceAuthPropertyTypeForMethods<PieceAuth>
  >
) => {
  const exampleBaseUrl = `https://api.example.com`;
  try {
    const baseUrlValue = auth ? baseUrl(auth) : undefined;
    const baseUrlValueWithoutTrailingSlash = baseUrlValue?.endsWith('/')
      ? baseUrlValue.slice(0, -1)
      : baseUrlValue;
    return baseUrlValueWithoutTrailingSlash ?? exampleBaseUrl;
  } catch (error) {
    //If baseUrl fails we stil want to return a valid baseUrl for description
    {
      return exampleBaseUrl;
    }
  }
};
type BaseUrlGetter<
  PieceAuth extends PieceAuthProperty | PieceAuthProperty[] | undefined
> = (
  auth?: AppConnectionValueForAuthProperty<
    ExtractPieceAuthPropertyTypeForMethods<PieceAuth>
  >
) => string;
export function createCustomApiCallAction<
  PieceAuth extends PieceAuthProperty | PieceAuthProperty[] | undefined
>({
  auth,
  baseUrl,
  authMapping,
  description,
  displayName,
  name,
  props,
  extraProps,
  authLocation = 'headers',
}: {
  auth?: PieceAuth;
  baseUrl: BaseUrlGetter<PieceAuth>;
  authMapping?: (
    auth: AppConnectionValueForAuthProperty<
      ExtractPieceAuthPropertyTypeForMethods<PieceAuth>
    >,
    propsValue: StaticPropsValue<any>
  ) => Promise<HttpHeaders | QueryParams>;
  //   add description as a parameter that can be null
  description?: string | null;
  displayName?: string | null;
  name?: string | null;
  props?: {
    url?: Partial<ReturnType<typeof Property.ShortText>>;
    method?: Partial<StaticDropdownProperty<HttpMethod, boolean>>;
    headers?: Partial<ReturnType<typeof Property.Object>>;
    queryParams?: Partial<ReturnType<typeof Property.Object>>;
    body?: Partial<ReturnType<typeof Property.Json>>;
    failsafe?: Partial<ReturnType<typeof Property.Checkbox>>;
    timeout?: Partial<ReturnType<typeof Property.Number>>;
  };
  extraProps?: InputPropertyMap;
  authLocation?: 'headers' | 'queryParams';
}) {
  return createAction({
    audience: 'human',
    name: name ? name : 'custom_api_call',
    displayName: displayName ? displayName : 'Custom API Call',
    description: description
      ? description
      : 'Make a custom API call to a specific endpoint',
    auth,
    requireAuth: auth ? true : false,
    props: {
      url: Property.DynamicProperties({
        auth,
        displayName: '',
        required: true,
        refreshers: [],
        props: async ({ auth }) => {
          return {
            url: Property.ShortText({
              displayName: 'URL',
              description: `You can either use the full URL or the relative path to the base URL
i.e ${getBaseUrlForDescription(baseUrl, auth)}/resource or /resource`,
              required: true,
              defaultValue: auth ? baseUrl(auth) : '',
              ...(props?.url ?? {}),
            }),
          };
        },
      }),
      method: Property.StaticDropdown({
        displayName: 'Method',
        required: true,
        options: {
          options: Object.values(HttpMethod).map((v) => {
            return {
              label: v,
              value: v,
            };
          }),
        },
        ...(props?.method ?? {}),
      }),
      headers: Property.Object({
        displayName: 'Headers',
        description:
          'Authorization headers are injected automatically from your connection.',
        required: true,
        ...(props?.headers ?? {}),
      }),
      queryParams: Property.Object({
        displayName: 'Query Parameters',
        required: true,
        ...(props?.queryParams ?? {}),
      }),
      body_type: Property.StaticDropdown({
        displayName: 'Body Type',
        required: false,
        defaultValue: 'none',
        options: {
          disabled: false,
          options: [
            {
              label: 'None',
              value: 'none',
            },
            {
              label: 'JSON',
              value: 'json',
            },
            {
              label: 'Form Data',
              value: 'form_data',
            },
            {
              label: 'Raw',
              value: 'raw',
            },
          ],
        },
      }),
      body: Property.DynamicProperties({
        auth,
        displayName: 'Body',
        refreshers: ['body_type'],
        required: false,
        props: async ({ body_type }) => {
          if (!body_type) return {};

          const bodyTypeInput = body_type as unknown as string;

          const fields: InputPropertyMap = {};

          switch (bodyTypeInput) {
            case 'none':
              break;
            case 'json':
              fields['data'] = Property.Json({
                displayName: 'JSON Body',
                required: true,
                ...(props?.body ?? {}),
              });
              break;
            case 'raw':
              fields['data'] = Property.LongText({
                displayName: 'Raw Body',
                required: true,
              });
              break;
            case 'form_data':
              fields['data'] = Property.Array({
                displayName: 'Form Data',
                required: true,
                properties: {
                  fieldName: Property.ShortText({
                    displayName: 'Field Name',
                    required: true,
                  }),
                  fieldType: Property.StaticDropdown({
                    displayName: 'Field Type',
                    required: true,
                    options: {
                      disabled: false,
                      options: [
                        { label: 'Text', value: 'text' },
                        { label: 'File', value: 'file' },
                      ],
                    },
                  }),
                  textFieldValue: Property.LongText({
                    displayName: 'Text Field Value',
                    required: false,
                  }),
                  fileFieldValue: Property.File({
                    displayName: 'File Field Value',
                    required: false,
                  }),
                },
              });
              break;
          }
          return fields;
        },
      }),
      response_is_binary: Property.Checkbox({
        displayName: 'Response is Binary ?',
        description: 'Enable for files like PDFs, images, etc.',
        required: false,
        defaultValue: false,
      }),
      failsafe: Property.Checkbox({
        displayName: 'No Error on Failure',
        required: false,
        ...(props?.failsafe ?? {}),
      }),
      timeout: Property.Number({
        displayName: 'Timeout (in seconds)',
        required: false,
        ...(props?.timeout ?? {}),
      }),
      followRedirects: Property.Checkbox({
        displayName: 'Follow redirects',
        required: false,
        defaultValue: false,
      }),
      ...extraProps,
    },

    run: async (context) => {
      const {
        method,
        url,
        headers,
        queryParams,
        body,
        body_type,
        failsafe,
        timeout,
        response_is_binary,
        followRedirects,
      } = context.propsValue;
      assertNotNullOrUndefined(method, 'Method');
      assertNotNullOrUndefined(url, 'URL');

      const authValue = !isNil(authMapping)
        ? await authMapping(context.auth, context.propsValue)
        : {};

      const urlValue = url['url'] as string;
      const fullUrl =
        urlValue.startsWith('http://') || urlValue.startsWith('https://')
          ? urlValue
          : joinBaseUrlWithRelativePath({
              baseUrl: baseUrl(context.auth),
              relativePath: urlValue,
            });
      const request: HttpRequest = {
        method,
        url: fullUrl,
        headers: {
          ...((headers ?? {}) as HttpHeaders),
          ...(authLocation === 'headers' || !isNil(authLocation)
            ? authValue
            : {}),
        },
        queryParams: {
          ...(authLocation === 'queryParams' ? (authValue as QueryParams) : {}),
          ...((queryParams as QueryParams) ?? {}),
        },
        timeout: timeout ? timeout * 1000 : 0,
        followRedirects,
      };

      // Set response type to arraybuffer if binary response is expected
      if (response_is_binary) {
        request.responseType = 'arraybuffer';
      }

      if (body) {
        if (body_type && body_type !== 'none') {
          const bodyInput = body['data'];
          if (body_type === 'form_data') {
            const formBodyInput = bodyInput as Array<{
              fieldName: string;
              fieldType: 'text' | 'file';
              textFieldValue?: string;
              fileFieldValue?: ApFile;
            }>;

            const formData = new FormData();

            for (const {
              fieldName,
              fieldType,
              textFieldValue,
              fileFieldValue,
            } of formBodyInput) {
              if (fieldType === 'text' && !isEmpty(textFieldValue)) {
                formData.append(fieldName, textFieldValue);
              } else if (fieldType === 'file' && !isEmpty(fileFieldValue)) {
                formData.append(fieldName, fileFieldValue!.data, {
                  filename: fileFieldValue?.filename,
                });
              }
            }
            request.body = formData;
            request.headers = { ...request.headers, ...formData.getHeaders() };
          } else {
            request.body = bodyInput;
          }
        } else if (!body_type) {
          request.body = body;
        }
      }

      try {
        const response = await httpClient.sendRequest(request);
        return await handleBinaryResponse(
          context.files,
          response.body,
          response.status,
          response.headers,
          response_is_binary
        );
      } catch (error) {
        if (failsafe) {
          return (error as HttpError).errorMessage();
        }
        throw error;
      }
    },
  });
}

export function is_chromium_installed(): boolean {
  const chromiumPath = '/usr/bin/chromium';
  return fs.existsSync(chromiumPath);
}

const handleBinaryResponse = async (
  files: FilesService,
  bodyContent: string | ArrayBuffer | Buffer,
  status: number,
  headers?: HttpHeaders,
  isBinary?: boolean
) => {
  let body;

  if (isBinary && isBinaryBody(bodyContent)) {
    const contentTypeValue = Array.isArray(headers?.['content-type'])
      ? headers['content-type'][0]
      : headers?.['content-type'];
    const fileExtension: string =
      contentTypeToExtension(contentTypeValue ?? '') || 'txt';

    let bufferData: Buffer;
    if (bodyContent instanceof ArrayBuffer) {
      bufferData = Buffer.from(new Uint8Array(bodyContent));
    } else if (Buffer.isBuffer(bodyContent)) {
      bufferData = bodyContent;
    } else {
      bufferData = Buffer.from(bodyContent);
    }

    body = await files.write({
      fileName: `output.${fileExtension}`,
      data: bufferData,
    });
  } else {
    body = bodyContent;
  }

  return { status, headers, body };
};

const isBinaryBody = (body: string | ArrayBuffer | Buffer) => {
  return body instanceof ArrayBuffer || Buffer.isBuffer(body);
};
