import type { Logger } from '$:/core/modules/utils/logger.js';
import { workspace } from '@services/wiki/wikiWorker/services';
import type { IWikiWorkspace, IWorkspace } from '@services/workspaces/interface';
import { backOff } from 'exponential-backoff';
import path from 'path';
import type { FileInfo } from 'tiddlywiki';
import type { Tiddler, Wiki } from 'tiddlywiki';
import { isFileLockError } from './utilities';

export type IFileSystemAdaptorCallback = (error: Error | null | string, fileInfo?: FileInfo | null) => void;

/**
 * Base filesystem adaptor that handles tiddler save/delete operations and sub-wiki routing.
 * This class can be used standalone or extended for additional functionality like file watching.
 */
export class FileSystemAdaptor {
  name = 'filesystem';
  supportsLazyLoading = false;
  wiki: Wiki;
  boot: typeof $tw.boot;
  logger: Logger;
  workspaceID: string;
  protected subWikisWithTag: IWikiWorkspace[] = [];
  /** Map of tagName -> subWiki for O(1) tag lookup instead of O(n) find */
  protected tagNameToSubWiki: Map<string, IWikiWorkspace> = new Map();
  /** Cached extension filters from $:/config/FileSystemExtensions. Requires restart to reflect changes. */
  protected extensionFilters: string[] | undefined;
  protected watchPathBase!: string;

  constructor(options: { boot?: typeof $tw.boot; wiki: Wiki }) {
    this.wiki = options.wiki;
    this.boot = options.boot ?? $tw.boot;
    this.logger = new $tw.utils.Logger('filesystem', { colour: 'blue' });

    if (!$tw.node) {
      throw new Error('filesystem adaptor only works in Node.js environment');
    }

    // Get workspace ID from preloaded tiddler
    this.workspaceID = this.wiki.getTiddlerText('$:/info/tidgi/workspaceID', '');

    if (this.boot.wikiTiddlersPath) {
      $tw.utils.createDirectory(this.boot.wikiTiddlersPath);
      this.watchPathBase = path.resolve(this.boot.wikiTiddlersPath);
    } else {
      this.logger.alert('filesystem: wikiTiddlersPath is not set!');
      this.watchPathBase = '';
    }

    // Initialize extension filters cache
    this.initializeExtensionFiltersCache();

    // Initialize sub-wikis cache
    void this.updateSubWikisCache();
  }

  /**
   * Initialize and cache extension filters from $:/config/FileSystemExtensions.
   */
  protected initializeExtensionFiltersCache(): void {
    if (this.wiki.tiddlerExists('$:/config/FileSystemExtensions')) {
      const extensionFiltersText = this.wiki.getTiddlerText('$:/config/FileSystemExtensions', '');
      this.extensionFilters = extensionFiltersText.split('\n').filter(line => line.trim().length > 0);
    }
  }

  /**
   * Update the cached sub-wikis list and rebuild tag lookup map
   */
  protected async updateSubWikisCache(): Promise<void> {
    try {
      if (!this.workspaceID) {
        this.subWikisWithTag = [];
        this.tagNameToSubWiki.clear();
        return;
      }

      const currentWorkspace = await workspace.get(this.workspaceID);
      if (!currentWorkspace) {
        this.subWikisWithTag = [];
        this.tagNameToSubWiki.clear();
        return;
      }

      const allWorkspaces = await workspace.getWorkspacesAsList();

      const subWikisWithTag = allWorkspaces.filter((workspaceItem: IWorkspace) =>
        'isSubWiki' in workspaceItem &&
        workspaceItem.isSubWiki &&
        workspaceItem.mainWikiID === currentWorkspace.id &&
        'tagName' in workspaceItem &&
        workspaceItem.tagName &&
        'wikiFolderLocation' in workspaceItem &&
        workspaceItem.wikiFolderLocation
      ) as IWikiWorkspace[];

      this.subWikisWithTag = subWikisWithTag;

      this.tagNameToSubWiki.clear();
      for (const subWiki of subWikisWithTag) {
        this.tagNameToSubWiki.set(subWiki.tagName!, subWiki);
      }
    } catch (error) {
      this.logger.alert('filesystem: Failed to update sub-wikis cache:', error);
    }
  }

  isReady(): boolean {
    return true;
  }

  getTiddlerInfo(tiddler: Tiddler): FileInfo | undefined {
    const title = tiddler.fields.title;
    return this.boot.files[title];
  }

  /**
   * Main routing logic: determine where a tiddler should be saved based on its tags.
   */
  async getTiddlerFileInfo(tiddler: Tiddler): Promise<FileInfo | null> {
    if (!this.boot.wikiTiddlersPath) {
      throw new Error('filesystem adaptor requires a valid wiki folder');
    }

    const title = tiddler.fields.title;
    const tags = tiddler.fields.tags ?? [];
    const fileInfo = this.boot.files[title];

    try {
      let matchingSubWiki: IWikiWorkspace | undefined;
      for (const tag of tags) {
        matchingSubWiki = this.tagNameToSubWiki.get(tag);
        if (matchingSubWiki) {
          break;
        }
      }

      if (matchingSubWiki) {
        return this.generateSubWikiFileInfo(tiddler, matchingSubWiki, fileInfo);
      } else {
        return this.generateDefaultFileInfo(tiddler, fileInfo);
      }
    } catch (error) {
      this.logger.alert(`filesystem: Error in getTiddlerFileInfo for "${title}":`, error);
      return this.generateDefaultFileInfo(tiddler, fileInfo);
    }
  }

  /**
   * Generate file info for sub-wiki directory
   */
  protected generateSubWikiFileInfo(tiddler: Tiddler, subWiki: IWikiWorkspace, fileInfo: FileInfo | undefined): FileInfo {
    const targetDirectory = subWiki.wikiFolderLocation;
    $tw.utils.createDirectory(targetDirectory);

    return $tw.utils.generateTiddlerFileInfo(tiddler, {
      directory: targetDirectory,
      pathFilters: undefined,
      extFilters: this.extensionFilters,
      wiki: this.wiki,
      fileInfo: fileInfo ? { ...fileInfo, overwrite: true } : { overwrite: true } as FileInfo,
    });
  }

  /**
   * Generate file info using default FileSystemPaths logic
   */
  protected generateDefaultFileInfo(tiddler: Tiddler, fileInfo: FileInfo | undefined): FileInfo {
    let pathFilters: string[] | undefined;

    if (this.wiki.tiddlerExists('$:/config/FileSystemPaths')) {
      const pathFiltersText = this.wiki.getTiddlerText('$:/config/FileSystemPaths', '');
      pathFilters = pathFiltersText.split('\n').filter(line => line.trim().length > 0);
    }

    return $tw.utils.generateTiddlerFileInfo(tiddler, {
      directory: this.boot.wikiTiddlersPath ?? '',
      pathFilters,
      extFilters: this.extensionFilters,
      wiki: this.wiki,
      fileInfo: fileInfo ? { ...fileInfo, overwrite: true } : { overwrite: true } as FileInfo,
    });
  }

  /**
   * Save a tiddler to the filesystem
   * Can be used with callback (legacy) or as async/await
   */
  async saveTiddler(tiddler: Tiddler, callback?: IFileSystemAdaptorCallback, options?: { tiddlerInfo?: Record<string, unknown> }): Promise<void> {
    try {
      const fileInfo = await this.getTiddlerFileInfo(tiddler);

      if (!fileInfo) {
        const error = new Error('No fileInfo returned from getTiddlerFileInfo');
        callback?.(error);
        throw error;
      }

      const savedFileInfo = await this.saveTiddlerWithRetry(tiddler, fileInfo);

      this.boot.files[tiddler.fields.title] = {
        ...savedFileInfo,
        isEditableFile: savedFileInfo.isEditableFile ?? true,
      };

      await new Promise<void>((resolve, reject) => {
        const cleanupOptions = {
          adaptorInfo: options?.tiddlerInfo as FileInfo | undefined,
          bootInfo: this.boot.files[tiddler.fields.title],
          title: tiddler.fields.title,
        };
        $tw.utils.cleanupTiddlerFiles(cleanupOptions, (cleanupError: Error | null, _cleanedFileInfo?: FileInfo) => {
          if (cleanupError) {
            reject(cleanupError);
            return;
          }
          resolve();
        });
      });

      callback?.(null, this.boot.files[tiddler.fields.title]);
    } catch (error) {
      const errorObject = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
      callback?.(errorObject);
      throw errorObject;
    }
  }

  /**
   * Load a tiddler - not needed as all tiddlers are loaded during boot
   */
  loadTiddler(_title: string, callback: IFileSystemAdaptorCallback): void {
    callback(null, null);
  }

  /**
   * Delete a tiddler from the filesystem
   * Can be used with callback (legacy) or as async/await
   */
  async deleteTiddler(title: string, callback?: IFileSystemAdaptorCallback, _options?: unknown): Promise<void> {
    const fileInfo = this.boot.files[title];

    if (!fileInfo) {
      callback?.(null, null);
      return;
    }

    try {
      await new Promise<void>((resolve, reject) => {
        $tw.utils.deleteTiddlerFile(fileInfo, (error: Error | null, deletedFileInfo?: FileInfo) => {
          if (error) {
            const errorCode = (error as NodeJS.ErrnoException).code;
            const errorSyscall = (error as NodeJS.ErrnoException).syscall;
            if ((errorCode === 'EPERM' || errorCode === 'EACCES') && errorSyscall === 'unlink') {
              this.logger.alert(`Server desynchronized. Error deleting file for deleted tiddler "${title}"`);
              callback?.(null, deletedFileInfo);
              resolve();
            } else {
              reject(error);
            }
            return;
          }

          this.removeTiddlerFileInfo(title);
          callback?.(null, null);
          resolve();
        });
      });
    } catch (error) {
      const errorObject = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
      callback?.(errorObject);
      throw errorObject;
    }
  }

  /**
   * Remove tiddler info from cache
   */
  removeTiddlerFileInfo(title: string): void {
    if (this.boot.files[title]) {
      // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
      delete this.boot.files[title];
    }
  }

  /**
   * Create an info tiddler to notify user about file save errors
   */
  protected createErrorNotification(title: string, error: Error, retryCount: number): void {
    const errorInfoTitle = `$:/temp/filesystem/error/${title}`;
    const errorTiddler = {
      title: errorInfoTitle,
      text:
        `Failed to save tiddler "${title}" after ${retryCount} retries.\n\nError: ${error.message}\n\nThe file might be locked by another process. Please close any applications using this file and try again.`,
      tags: ['$:/tags/Alert'],
      type: 'text/vnd.tiddlywiki',
      'error-type': 'file-save-error',
      'original-title': title,
      timestamp: new Date().toISOString(),
    };

    this.wiki.addTiddler(errorTiddler);
    this.logger.alert(`filesystem: Created error notification for "${title}"`);
  }

  /**
   * Save tiddler with exponential backoff retry for file lock errors
   */
  protected async saveTiddlerWithRetry(
    tiddler: Tiddler,
    fileInfo: FileInfo,
    options: { maxRetries?: number; initialDelay?: number; maxDelay?: number } = {},
  ): Promise<FileInfo> {
    const maxRetries = options.maxRetries ?? 10;
    const initialDelay = options.initialDelay ?? 50;
    const maxDelay = options.maxDelay ?? 2000;

    try {
      return await backOff(
        async () => {
          return await new Promise<FileInfo>((resolve, reject) => {
            $tw.utils.saveTiddlerToFile(tiddler, fileInfo, (saveError: Error | null, savedFileInfo?: FileInfo) => {
              if (saveError) {
                reject(saveError);
                return;
              }
              if (!savedFileInfo) {
                reject(new Error('No fileInfo returned from saveTiddlerToFile'));
                return;
              }
              resolve(savedFileInfo);
            });
          });
        },
        {
          numOfAttempts: maxRetries,
          startingDelay: initialDelay,
          timeMultiple: 2,
          maxDelay,
          delayFirstAttempt: false,
          jitter: 'none',
          retry: (error: Error, attemptNumber: number) => {
            const errorCode = (error as NodeJS.ErrnoException).code;

            if (isFileLockError(errorCode)) {
              this.logger.log(
                `filesystem: File "${fileInfo.filepath}" is locked (${errorCode}), retrying (attempt ${attemptNumber}/${maxRetries})`,
              );
              return true;
            }

            this.logger.alert(`filesystem: Error saving "${tiddler.fields.title}":`, error);
            this.createErrorNotification(tiddler.fields.title, error, attemptNumber);
            return false;
          },
        },
      );
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      const finalError = new Error(`Failed to save "${tiddler.fields.title}": ${errorMessage}`);
      this.createErrorNotification(tiddler.fields.title, finalError, maxRetries);
      throw finalError;
    }
  }
}
