import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk';
import { glob } from 'glob';
import { readFile, stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import yaml from 'yaml';

export interface BaseOptions {
  configDirectory: string;
  apiKey?: string;
  instanceUrl?: string;
}

export interface AuthDto {
  instanceUrl: string;
  apiKey: string;
}

export const authenticate = async (options: BaseOptions): Promise<void> => {
  const { configDirectory: configDir, instanceUrl, apiKey } = options;

  // provided in command
  if (instanceUrl && apiKey) {
    await connect(instanceUrl, apiKey);
    return;
  }

  // fallback to file
  const config = await readAuthFile(configDir);
  await connect(config.instanceUrl, config.apiKey);
};

export const connect = async (instanceUrl: string, apiKey: string): Promise<void> => {
  const wellKnownUrl = new URL('.well-known/immich', instanceUrl);
  try {
    const wellKnown = await fetch(wellKnownUrl).then((response) => response.json());
    const endpoint = new URL(wellKnown.api.endpoint, instanceUrl).toString();
    if (endpoint !== instanceUrl) {
      console.debug(`Discovered API at ${endpoint}`);
    }
    instanceUrl = endpoint;
  } catch {
    // noop
  }

  defaults.baseUrl = instanceUrl;
  defaults.headers = { 'x-api-key': apiKey };

  const [error] = await withError(getMyUserInfo());
  if (isHttpError(error)) {
    logError(error, 'Failed to connect to server');
    process.exit(1);
  }
};

export const logError = (error: unknown, message: string) => {
  if (isHttpError(error)) {
    console.error(`${message}: ${error.status}`);
    console.error(JSON.stringify(error.data, undefined, 2));
  } else {
    console.error(`${message} - ${error}`);
  }
};

export const getAuthFilePath = (dir: string) => join(dir, 'auth.yml');

export const readAuthFile = async (dir: string) => {
  try {
    const data = await readFile(getAuthFilePath(dir));
    // TODO add class-transform/validation
    return yaml.parse(data.toString()) as AuthDto;
  } catch (error: Error | any) {
    if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
      console.log('No auth file exists. Please login first.');
      process.exit(1);
    }
    throw error;
  }
};

export const writeAuthFile = async (dir: string, auth: AuthDto) =>
  writeFile(getAuthFilePath(dir), yaml.stringify(auth), { mode: 0o600 });

export const withError = async <T>(promise: Promise<T>): Promise<[Error, undefined] | [undefined, T]> => {
  try {
    const result = await promise;
    return [undefined, result];
  } catch (error: Error | any) {
    return [error, undefined];
  }
};

export interface CrawlOptions {
  pathsToCrawl: string[];
  recursive?: boolean;
  includeHidden?: boolean;
  exclusionPatterns?: string[];
  extensions: string[];
}
export const crawl = async (options: CrawlOptions): Promise<string[]> => {
  const { extensions: extensionsWithPeriod, recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options;
  const extensions = extensionsWithPeriod.map((extension) => extension.replace('.', ''));

  if (!pathsToCrawl) {
    return [];
  }

  const patterns: string[] = [];
  const crawledFiles: string[] = [];

  for await (const currentPath of pathsToCrawl) {
    try {
      const stats = await stat(currentPath);
      if (stats.isFile() || stats.isSymbolicLink()) {
        crawledFiles.push(currentPath);
      } else {
        patterns.push(currentPath);
      }
    } catch (error: any) {
      if (error.code === 'ENOENT') {
        patterns.push(currentPath);
      } else {
        throw error;
      }
    }
  }

  let searchPattern: string;
  if (patterns.length === 1) {
    searchPattern = patterns[0];
  } else if (patterns.length === 0) {
    return crawledFiles;
  } else {
    searchPattern = '{' + patterns.join(',') + '}';
  }

  if (recursive) {
    searchPattern = searchPattern + '/**/';
  }

  searchPattern = `${searchPattern}/*.{${extensions.join(',')}}`;

  const globbedFiles = await glob(searchPattern, {
    absolute: true,
    nocase: true,
    nodir: true,
    dot: includeHidden,
    ignore: exclusionPatterns,
  });

  return [...crawledFiles, ...globbedFiles].sort();
};
