import {
  atomStorage,
  BaseService,
  type BaseServiceContext,
  getEventSenderMetadata,
} from '@learnitive/base-utils';
import type {
  ThemeConfig,
  ThemeManager,
} from '@learnitive/color-scheme-manager';
import {
  isAssetLocationPreference,
  SERVICE_NAME,
  SIDEBAR_DEFAULT_WIDTH,
} from '@learnitive/constants';
import { T } from '@learnitive/mini-js-utils';
import type {
  AssetLocationPreference,
  BaseSyncDatabaseService,
  ScopedEmitter,
} from '@learnitive/types';
import type {
  AppAlertDialogProps,
  DialogSingleInputProps,
  DialogSingleSelectProps,
} from '@learnitive/ui-components';
import { atom, type PrimitiveAtom } from 'jotai';
import { atomEffect } from 'jotai-effect';
import {
  type FileTreeExpandedPathsByWorkspace,
  type FileTreeExpansionAction,
  reduceFileTreeExpansion,
} from './file-tree-expansion-state';

type Route = 'omni-home' | 'omni-command' | 'omni-filtered';

const AssetLocationPreferenceValidator = {
  validate: isAssetLocationPreference,
  typeName: 'asset-location-preference',
};

/**
 * Column visibility overrides for the workspace notes table, keyed by column
 * id. Columns absent from the record use the table's own defaults.
 */
export type NotesTableColumnVisibility = Record<string, boolean>;

const NotesTableColumnVisibilityValidator = {
  validate: (value: unknown): value is NotesTableColumnVisibility =>
    typeof value === 'object' &&
    value !== null &&
    !Array.isArray(value) &&
    Object.values(value).every((visible) => typeof visible === 'boolean'),
  typeName: 'notes-table-column-visibility',
};

const FileTreeExpandedPathsByWorkspaceValidator = {
  validate: (value: unknown): value is FileTreeExpandedPathsByWorkspace =>
    typeof value === 'object' &&
    value !== null &&
    !Array.isArray(value) &&
    Object.values(value).every(
      (paths) =>
        Array.isArray(paths) && paths.every((path) => typeof path === 'string'),
    ),
  typeName: 'file-tree-expanded-paths-by-workspace',
};

function determineOmniSearchRoute(input: string, currentRoute: Route): Route {
  switch (currentRoute) {
    case 'omni-home': {
      if (input.startsWith('>')) {
        return 'omni-command';
      }
      return 'omni-filtered';
    }
    case 'omni-command': {
      if (!input.startsWith('>')) {
        if (input.trim() === '') {
          return 'omni-home';
        }
        return 'omni-filtered';
      }
      return 'omni-command';
    }
    case 'omni-filtered': {
      if (input.trim() === '') {
        return 'omni-home';
      }
      return 'omni-filtered';
    }
    default: {
      return 'omni-home';
    }
  }
}

/**
 * Manages UI state such as theme preferences, dialogs, and omni-search state
 */
export class WorkbenchStateService extends BaseService {
  static deps = ['syncDatabase'] as const;

  private $_wideEditor: PrimitiveAtom<boolean> | undefined;
  private $_sidebarOpen: PrimitiveAtom<boolean> | undefined;
  private $_sidebarWidth: PrimitiveAtom<number> | undefined;
  private $_linkedMentionsCollapsed: PrimitiveAtom<boolean> | undefined;
  private $_showNoteFilesOnlyInSidebar: PrimitiveAtom<boolean> | undefined;
  private $_pwaOpenInAppPromptSeen: PrimitiveAtom<boolean> | undefined;
  private $_fileTreeExpandedPathsByWorkspace:
    | PrimitiveAtom<FileTreeExpandedPathsByWorkspace>
    | undefined;
  private $_assetLocationPreference:
    | PrimitiveAtom<AssetLocationPreference>
    | undefined;
  private $_notesTableColumnVisibility:
    | PrimitiveAtom<NotesTableColumnVisibility>
    | undefined;

  $openWsDialog = atom(false);
  $openOmniSearch = atom(false);
  $projectToolsOpen = atom(false);
  $projectToolsTab = atom<'text' | 'symbols' | 'doctor' | 'refactor'>('text');
  $projectToolsTextScope = atom<'project' | 'folder' | 'file'>('project');
  $projectNavigatorOpen = atom(false);
  $latexEditorMode = atom<'source' | 'visual'>('source');
  $pdfFocusRequest = atom(0);
  $themePref = atom<ThemeConfig['defaultPreference']>('system');
  /**
   * True once this tab is known to run an older app version than another tab
   * (storage schema handshake). The app blocks the tab behind a reload
   * dialog; there is deliberately no way to unset it short of reloading.
   */
  $staleTab = atom(false);
  $singleInputDialog = atom<
    | undefined
    | ({
        dialogId: `dialog::${string}`;
      } & Omit<DialogSingleInputProps, 'open' | 'setOpen'>)
  >(undefined);
  $singleSelectDialog = atom<
    | undefined
    | ({
        dialogId: `dialog::${string}`;
      } & Omit<DialogSingleSelectProps, 'open' | 'setOpen'>)
  >(undefined);
  $alertDialog = atom<
    | undefined
    | ({ dialogId: `dialog::${string}` } & Omit<
        AppAlertDialogProps,
        'open' | 'setOpen'
      >)
  >();
  $omniSearchInput = atom('');
  $omniSearchRoute = atom<Route>('omni-home');
  $openAllFiles = atom(false);
  $allFilesSearchInput = atom('');

  $cleanSearchTerm = atom((get) => {
    const search = get(this.$omniSearchInput);
    const route = get(this.$omniSearchRoute);

    if (route === 'omni-command') {
      return search.slice(1).trim().toLowerCase();
    }
    return search.trim().toLowerCase();
  });

  constructor(
    context: BaseServiceContext,
    private dep: {
      syncDatabase: BaseSyncDatabaseService;
    },
    private config: {
      themeManager: ThemeManager;
      emitter: ScopedEmitter<'event::app:reload-ui' | 'event::app:stale-tab'>;
    },
  ) {
    super(SERVICE_NAME.workbenchStateService, context, dep);
    this.store.set(this.$themePref, this.config.themeManager.currentPreference);
  }

  hookMount() {
    this.config.emitter.on(
      'event::app:stale-tab',
      () => {
        this.store.set(this.$staleTab, true);
      },
      this.abortSignal,
    );
    this.addCleanup(
      this.config.themeManager.onThemeChange(({ preference }) => {
        this.store.set(this.$themePref, preference);
      }),
      this.store.sub(
        atomEffect((get, set) => {
          const open = get(this.$openAllFiles);
          if (!open) {
            set(this.$allFilesSearchInput, '');
          }
        }),
        () => {},
      ),
      this.store.sub(
        atomEffect((get, set) => {
          const open = get(this.$openOmniSearch);
          if (!open) {
            set(this.$omniSearchInput, '');
          }
        }),
        () => {},
      ),
      this.store.sub(this.$omniSearchInput, () => {
        const input = this.store.get(this.$omniSearchInput);
        const currentRoute = this.store.get(this.$omniSearchRoute);
        const newRoute = determineOmniSearchRoute(input, currentRoute);
        if (newRoute !== currentRoute) {
          this.store.set(this.$omniSearchRoute, newRoute);
        }
      }),
    );
  }

  public changeThemePreference(preference: ThemeConfig['defaultPreference']) {
    this.config.themeManager.setPreference(preference);
  }

  public resetOmniSearch() {
    this.store.set(this.$omniSearchInput, '');
    this.store.set(this.$omniSearchRoute, 'omni-home');
  }

  public goToCommandRoute() {
    this.store.set(this.$openOmniSearch, true);
    this.store.set(this.$omniSearchInput, '>');
  }

  public reloadUi() {
    this.config.emitter.emit('event::app:reload-ui', {
      sender: getEventSenderMetadata({ tag: this.name }),
    });
  }

  public setFileTreeDirectoryExpanded(
    workspaceName: string,
    path: string,
    expanded: boolean,
  ) {
    this.updateFileTreeExpansion({
      type: 'set-directory',
      workspaceName,
      path,
      expanded,
    });
  }

  public revealFileTreePaths(workspaceName: string, paths: readonly string[]) {
    this.updateFileTreeExpansion({ type: 'reveal', workspaceName, paths });
  }

  public collapseFileTree(
    workspaceName: string,
    keepExpandedPaths: readonly string[],
  ) {
    this.updateFileTreeExpansion({
      type: 'collapse-all',
      workspaceName,
      keepExpandedPaths,
    });
  }

  private updateFileTreeExpansion(action: FileTreeExpansionAction) {
    this.store.set(this.$fileTreeExpandedPathsByWorkspace, (current) =>
      reduceFileTreeExpansion(current, action),
    );
  }

  get $wideEditor() {
    if (!this.$_wideEditor) {
      this.$_wideEditor = atomStorage({
        serviceName: this.name,
        key: 'wide-editor',
        initValue: true,
        syncDb: this.dep.syncDatabase,
        validator: T.Boolean,
        logger: this.logger,
      });
    }
    return this.$_wideEditor;
  }

  get $sidebarOpen() {
    if (!this.$_sidebarOpen) {
      this.$_sidebarOpen = atomStorage({
        serviceName: this.name,
        key: 'sidebar-open',
        initValue: true,
        syncDb: this.dep.syncDatabase,
        validator: T.Boolean,
        logger: this.logger,
      });
    }
    return this.$_sidebarOpen;
  }

  get $sidebarWidth() {
    if (!this.$_sidebarWidth) {
      this.$_sidebarWidth = atomStorage({
        serviceName: this.name,
        key: 'sidebar-width',
        initValue: SIDEBAR_DEFAULT_WIDTH,
        syncDb: this.dep.syncDatabase,
        validator: {
          validate: (value): value is number =>
            typeof value === 'number' && Number.isFinite(value),
          typeName: 'finite-number',
        },
        logger: this.logger,
      });
    }
    return this.$_sidebarWidth;
  }

  get $linkedMentionsCollapsed() {
    if (!this.$_linkedMentionsCollapsed) {
      this.$_linkedMentionsCollapsed = atomStorage({
        serviceName: this.name,
        key: 'linked-mentions-collapsed',
        initValue: false,
        syncDb: this.dep.syncDatabase,
        validator: T.Boolean,
        logger: this.logger,
      });
    }
    return this.$_linkedMentionsCollapsed;
  }

  get $showNoteFilesOnlyInSidebar() {
    if (!this.$_showNoteFilesOnlyInSidebar) {
      this.$_showNoteFilesOnlyInSidebar = atomStorage({
        serviceName: this.name,
        key: 'show-note-files-only-in-sidebar',
        initValue: false,
        syncDb: this.dep.syncDatabase,
        validator: T.Boolean,
        logger: this.logger,
      });
    }
    return this.$_showNoteFilesOnlyInSidebar;
  }

  get $pwaOpenInAppPromptSeen() {
    if (!this.$_pwaOpenInAppPromptSeen) {
      this.$_pwaOpenInAppPromptSeen = atomStorage({
        serviceName: this.name,
        key: 'pwa-open-in-app-prompt-seen',
        initValue: false,
        syncDb: this.dep.syncDatabase,
        validator: T.Boolean,
        logger: this.logger,
      });
    }
    return this.$_pwaOpenInAppPromptSeen;
  }

  get $fileTreeExpandedPathsByWorkspace() {
    if (!this.$_fileTreeExpandedPathsByWorkspace) {
      this.$_fileTreeExpandedPathsByWorkspace = atomStorage({
        serviceName: this.name,
        key: 'file-tree-expanded-paths-by-workspace',
        initValue: {},
        syncDb: this.dep.syncDatabase,
        validator: FileTreeExpandedPathsByWorkspaceValidator,
        logger: this.logger,
      });
    }
    return this.$_fileTreeExpandedPathsByWorkspace;
  }

  get $notesTableColumnVisibility() {
    if (!this.$_notesTableColumnVisibility) {
      this.$_notesTableColumnVisibility = atomStorage({
        serviceName: this.name,
        key: 'notes-table-column-visibility',
        initValue: {},
        syncDb: this.dep.syncDatabase,
        validator: NotesTableColumnVisibilityValidator,
        logger: this.logger,
      });
    }
    return this.$_notesTableColumnVisibility;
  }

  get $assetLocationPreference() {
    if (!this.$_assetLocationPreference) {
      this.$_assetLocationPreference = atomStorage({
        serviceName: this.name,
        key: 'asset-location-preference',
        initValue: 'assets-folder',
        syncDb: this.dep.syncDatabase,
        validator: AssetLocationPreferenceValidator,
        logger: this.logger,
      });
    }
    return this.$_assetLocationPreference;
  }
}
