import {
  BaseService,
  type BaseServiceContext,
  isPlainObject,
  throwAppError,
} from '@learnitive/base-utils';
import {
  DATABASE_TABLE_NAME,
  SERVICE_NAME,
  WORKSPACE_STORAGE_TYPE,
} from '@learnitive/constants';
import type {
  BaseDatabaseService,
  WorkspaceDatabaseQueryOptions,
  WorkspaceInfo,
} from '@learnitive/types';
import { atom } from 'jotai';
import type { ProjectEntitlementService } from './project-entitlement-service';

const JOINED_COLLABORATION_METADATA_KEY = 'learnitiveCollaboration';
const PROJECT_CREATION_LOCK_NAME = 'learnitive-free-project-create-v1';

type CreateWorkspaceInfoOptions = {
  /** Joined collaboration materialization and metadata-only renames do not create a new user project slot. */
  bypassProjectLimit?: boolean;
};

type BrowserProjectLimitStatus = {
  current: number;
  limit: number | null;
  reached: boolean;
};

function isJoinedCollaborationWorkspace(info: Pick<WorkspaceInfo, 'metadata'>): boolean {
  const value = info.metadata?.[JOINED_COLLABORATION_METADATA_KEY];
  return isPlainObject(value) && value.source === 'joined';
}

function countsTowardBrowserProjectLimit(
  info: Pick<WorkspaceInfo, 'type' | 'metadata' | 'deleted'>,
): boolean {
  if (info.deleted || isJoinedCollaborationWorkspace(info)) return false;
  return info.type === WORKSPACE_STORAGE_TYPE.Browser
    || info.type === WORKSPACE_STORAGE_TYPE.BrowserOPFS
    || info.type === WORKSPACE_STORAGE_TYPE.NativeFS;
}

function stableLocalProjectId(): string {
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID();
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
}

function withCanonicalProjectIdentity(
  info: Omit<WorkspaceInfo, 'lastModified' | 'deleted'>,
): Omit<WorkspaceInfo, 'lastModified' | 'deleted'> {
  if (!countsTowardBrowserProjectLimit({ ...info, deleted: false })) return info;
  const metadata = isPlainObject(info.metadata) ? { ...info.metadata } : {};
  if (typeof metadata.projectId !== 'string' || !metadata.projectId.trim()) metadata.projectId = stableLocalProjectId();
  if (typeof metadata.projectSlug !== 'string' || !metadata.projectSlug.trim()) metadata.projectSlug = info.name;
  if (typeof metadata.projectDisplayName !== 'string' || !metadata.projectDisplayName.trim()) metadata.projectDisplayName = info.name;
  if (typeof metadata.projectCreatedAt !== 'number' || !Number.isFinite(metadata.projectCreatedAt)) metadata.projectCreatedAt = Date.now();
  return { ...info, metadata };
}

/**
 * Provides API operations for managing workspace metadata and info
 */
export class WorkspaceOpsService extends BaseService {
  static deps = ['database', 'projectEntitlements'] as const;

  /**
   * Bumps on every workspaceInfo change (create, update, delete). Soft-deletes
   * and restores go through `updateEntry`, which emits an 'update' change rather
   * than 'delete', so consumers that track the visible (non-deleted) workspace
   * set must treat any change as significant.
   */
  $workspaceInfoChange = atom(0);

  private workspaceInfoCache = new Map<string, WorkspaceInfo>();
  private projectCreationQueue: Promise<void> = Promise.resolve();

  constructor(
    context: BaseServiceContext,
    private dep: {
      database: BaseDatabaseService;
      projectEntitlements: ProjectEntitlementService;
    },
  ) {
    super(SERVICE_NAME.workspaceOpsService, context, dep);
  }

  async hookMount(): Promise<void> {
    this.database.subscribe(
      { tableName: DATABASE_TABLE_NAME.workspaceInfo },
      () => {
        this.invalidateCache();
        this.store.set(this.$workspaceInfoChange, (v) => v + 1);
      },
      this.abortSignal,
    );
  }

  /**
   * Retrieves workspace info for a given workspace name, optionally restricted by type.
   * Uses an internal cache to speed up repeated queries.
   */
  public async getWorkspaceInfo(
    wsName: string,
    options?: WorkspaceDatabaseQueryOptions,
  ): Promise<WorkspaceInfo | undefined> {
    await this.mountPromise;

    let wsInfo = this.workspaceInfoCache.get(wsName);
    if (!wsInfo) {
      const result = await this.database.getEntry(wsName, {
        tableName: DATABASE_TABLE_NAME.workspaceInfo,
      });

      if (!result.found) {
        return undefined;
      }

      wsInfo = result.value as WorkspaceInfo;
      this.workspaceInfoCache.set(wsName, wsInfo);
    }

    if (!options?.allowDeleted && wsInfo?.deleted) {
      return undefined;
    }

    if (options?.type && wsInfo.type !== options.type) {
      return undefined;
    }

    return wsInfo;
  }

  public async getBrowserProjectLimitStatus(): Promise<BrowserProjectLimitStatus> {
    await this.mountPromise;
    const entries = await this.ownedBrowserProjects();
    const limit = await this.dep.projectEntitlements.projectLimit();
    return { current: entries.length, limit, reached: limit !== null && entries.length >= limit };
  }

  public async assertProjectCreationAllowed(): Promise<BrowserProjectLimitStatus> {
    const status = await this.getBrowserProjectLimitStatus();
    if (status.limit !== null && status.reached) {
      throwAppError(
        'error::workspace:project-limit-reached',
        `Free includes up to ${status.limit} projects. Delete a project or upgrade to Pro to create another.`,
        { current: status.current, limit: status.limit },
      );
    }
    return status;
  }

  public async createWorkspaceInfo(
    info: Omit<WorkspaceInfo, 'lastModified' | 'deleted'>,
    options: CreateWorkspaceInfoOptions = {},
  ): Promise<WorkspaceInfo | undefined> {
    await this.mountPromise;
    const canonicalInfo = withCanonicalProjectIdentity(info);

    const create = async (): Promise<WorkspaceInfo | undefined> => {
      const wsName = canonicalInfo.name;
      const result = await this.database.updateEntry(
        wsName,
        (existing) => {
          if (existing.found && !(existing.value as WorkspaceInfo)?.deleted) {
            throwAppError(
              'error::workspace:already-exists',
              'Cannot create workspace as it already exists',
              { wsName },
            );
          }

          const value: WorkspaceInfo = {
            ...canonicalInfo,
            deleted: false,
            lastModified: Date.now(),
          };

          return { value };
        },
        { tableName: DATABASE_TABLE_NAME.workspaceInfo },
      );

      return result.found ? (result.value as WorkspaceInfo) : undefined;
    };

    if (
      options.bypassProjectLimit ||
      !countsTowardBrowserProjectLimit({ ...canonicalInfo, deleted: false })
    ) {
      return create();
    }
    return this.withProjectCreationLock(async () => {
      await this.assertProjectCreationAllowed();
      return create();
    });
  }

  public async deleteWorkspaceInfo(wsName: string): Promise<void> {
    await this.mountPromise;
    await this.database.updateEntry(
      wsName,
      (existing) => {
        if (!existing.found) {
          throwAppError(
            'error::workspace:not-found',
            'Cannot delete workspace as it does not exist',
            {
              wsName,
            },
          );
        }

        const value: WorkspaceInfo = {
          ...(existing.value as WorkspaceInfo),
          lastModified: Date.now(),
          deleted: true,
        };

        return {
          value,
        };
      },
      { tableName: DATABASE_TABLE_NAME.workspaceInfo },
    );
  }

  public async updateWorkspaceInfo(
    name: string,
    update: (wsInfo: WorkspaceInfo) => WorkspaceInfo,
  ): Promise<WorkspaceInfo | undefined> {
    await this.mountPromise;
    const result = await this.database.updateEntry(
      name,
      (existing) => {
        if (!existing.found) {
          throwAppError(
            'error::workspace:not-found',
            'Cannot update workspace as it does not exist',
            {
              wsName: name,
            },
          );
        }

        const existingValue = existing.value as WorkspaceInfo;
        const value = {
          ...existingValue,
          ...update(existingValue),
          lastModified: Date.now(),
        };

        return {
          value,
        };
      },
      {
        tableName: DATABASE_TABLE_NAME.workspaceInfo,
      },
    );

    if (result.found) {
      return result.value as WorkspaceInfo;
    }

    return undefined;
  }

  public async getAllWorkspaces(options?: {
    type?: WorkspaceInfo['type'];
    allowDeleted?: boolean;
  }): Promise<WorkspaceInfo[]> {
    await this.mountPromise;
    const result = (await this.database.getAllEntries({
      tableName: DATABASE_TABLE_NAME.workspaceInfo,
    })) as WorkspaceInfo[];

    return result.filter((wsInfo) => {
      if (!options?.allowDeleted && wsInfo?.deleted) {
        return false;
      }
      if (options?.type) {
        return wsInfo.type === options.type;
      }
      return true;
    });
  }

  public async getWorkspaceMetadata(
    name: string,
  ): Promise<Record<string, unknown>> {
    await this.mountPromise;
    const result = (await this.getWorkspaceInfo(name))?.metadata;

    if (!result || !isPlainObject(result)) {
      return {};
    }
    return result;
  }

  public async updateWorkspaceMetadata(
    name: string,
    metadata: (
      existingMetadata: WorkspaceInfo['metadata'],
    ) => WorkspaceInfo['metadata'],
  ): Promise<boolean> {
    await this.mountPromise;
    await this.updateWorkspaceInfo(name, (wsInfo) => {
      const finalMetadata = metadata(wsInfo.metadata ?? {});

      if (!isPlainObject(finalMetadata)) {
        throwAppError(
          'error::workspace:invalid-metadata',
          `Invalid metadata for workspace ${name}`,
          {
            wsName: name,
          },
        );
      }

      return {
        ...wsInfo,
        metadata: finalMetadata,
      };
    });

    return true;
  }

  private async ownedBrowserProjects(): Promise<WorkspaceInfo[]> {
    const entries = (await this.database.getAllEntries({
      tableName: DATABASE_TABLE_NAME.workspaceInfo,
    })) as WorkspaceInfo[];
    return entries.filter(countsTowardBrowserProjectLimit);
  }

  private async withProjectCreationLock<T>(operation: () => Promise<T>): Promise<T> {
    const locks = typeof navigator === 'undefined'
      ? undefined
      : (navigator as Navigator & {
          locks?: {
            request<TResult>(name: string, callback: () => Promise<TResult>): Promise<TResult>;
          };
        }).locks;
    if (locks) return locks.request(PROJECT_CREATION_LOCK_NAME, operation);

    const previous = this.projectCreationQueue;
    let release!: () => void;
    this.projectCreationQueue = new Promise<void>((resolve) => {
      release = resolve;
    });
    await previous;
    try {
      return await operation();
    } finally {
      release();
    }
  }

  public invalidateCache(): void {
    this.workspaceInfoCache.clear();
  }

  private get database() {
    return this.dep.database;
  }
}
