import { eq, and, isNull, desc, or } from 'drizzle-orm';
import { db } from '@/db';
import { images, flows } from '@banatie/database';
import type { AliasResolution, Image } from '@/types/models';
import { isTechnicalAlias } from '@/utils/constants/aliases';
import {
  validateAliasFormat,
  validateAliasNotReserved,
} from '@/utils/validators';
import { ERROR_MESSAGES } from '@/utils/constants';

export class AliasService {
  async resolve(
    alias: string,
    projectId: string,
    flowId?: string
  ): Promise<AliasResolution | null> {
    const formatResult = validateAliasFormat(alias);
    if (!formatResult.valid) {
      throw new Error(formatResult.error!.message);
    }

    if (isTechnicalAlias(alias)) {
      if (!flowId) {
        throw new Error(ERROR_MESSAGES.TECHNICAL_ALIAS_REQUIRES_FLOW);
      }
      return await this.resolveTechnicalAlias(alias, flowId, projectId);
    }

    if (flowId) {
      const flowResolution = await this.resolveFlowAlias(alias, flowId, projectId);
      if (flowResolution) {
        return flowResolution;
      }
    }

    return await this.resolveProjectAlias(alias, projectId);
  }

  private async resolveTechnicalAlias(
    alias: string,
    flowId: string,
    projectId: string
  ): Promise<AliasResolution | null> {
    let image: Image | undefined;

    switch (alias) {
      case '@last':
        image = await this.getLastGeneratedInFlow(flowId, projectId);
        break;

      case '@first':
        image = await this.getFirstGeneratedInFlow(flowId, projectId);
        break;

      case '@upload':
        image = await this.getLastUploadedInFlow(flowId, projectId);
        break;

      default:
        return null;
    }

    if (!image) {
      return null;
    }

    return {
      imageId: image.id,
      scope: 'technical',
      flowId,
      image,
    };
  }

  private async resolveFlowAlias(
    alias: string,
    flowId: string,
    projectId: string
  ): Promise<AliasResolution | null> {
    const flow = await db.query.flows.findFirst({
      where: and(eq(flows.id, flowId), eq(flows.projectId, projectId)),
    });

    if (!flow) {
      return null;
    }

    const flowAliases = flow.aliases as Record<string, string>;
    const imageId = flowAliases[alias];

    if (!imageId) {
      return null;
    }

    const image = await db.query.images.findFirst({
      where: and(
        eq(images.id, imageId),
        eq(images.projectId, projectId),
        isNull(images.deletedAt)
      ),
    });

    if (!image) {
      return null;
    }

    return {
      imageId: image.id,
      scope: 'flow',
      flowId,
      image,
    };
  }

  private async resolveProjectAlias(
    alias: string,
    projectId: string
  ): Promise<AliasResolution | null> {
    // Project aliases can exist on images with or without flowId
    // Per spec: images with project alias should be resolvable at project level
    const image = await db.query.images.findFirst({
      where: and(
        eq(images.projectId, projectId),
        eq(images.alias, alias),
        isNull(images.deletedAt)
      ),
    });

    if (!image) {
      return null;
    }

    return {
      imageId: image.id,
      scope: 'project',
      image,
    };
  }

  private async getLastGeneratedInFlow(
    flowId: string,
    projectId: string
  ): Promise<Image | undefined> {
    // Check both flowId and pendingFlowId to support lazy flow pattern (Section 4.1)
    // Images may have pendingFlowId before the flow record is created
    return await db.query.images.findFirst({
      where: and(
        or(eq(images.flowId, flowId), eq(images.pendingFlowId, flowId)),
        eq(images.projectId, projectId),
        eq(images.source, 'generated'),
        isNull(images.deletedAt)
      ),
      orderBy: [desc(images.createdAt)],
    });
  }

  private async getFirstGeneratedInFlow(
    flowId: string,
    projectId: string
  ): Promise<Image | undefined> {
    // Check both flowId and pendingFlowId to support lazy flow pattern (Section 4.1)
    const allImages = await db.query.images.findMany({
      where: and(
        or(eq(images.flowId, flowId), eq(images.pendingFlowId, flowId)),
        eq(images.projectId, projectId),
        eq(images.source, 'generated'),
        isNull(images.deletedAt)
      ),
      orderBy: [images.createdAt],
      limit: 1,
    });

    return allImages[0];
  }

  private async getLastUploadedInFlow(
    flowId: string,
    projectId: string
  ): Promise<Image | undefined> {
    // Check both flowId and pendingFlowId to support lazy flow pattern (Section 4.1)
    return await db.query.images.findFirst({
      where: and(
        or(eq(images.flowId, flowId), eq(images.pendingFlowId, flowId)),
        eq(images.projectId, projectId),
        eq(images.source, 'uploaded'),
        isNull(images.deletedAt)
      ),
      orderBy: [desc(images.createdAt)],
    });
  }

  async validateAliasForAssignment(alias: string, _projectId: string, _flowId?: string): Promise<void> {
    const formatResult = validateAliasFormat(alias);
    if (!formatResult.valid) {
      throw new Error(formatResult.error!.message);
    }

    const reservedResult = validateAliasNotReserved(alias);
    if (!reservedResult.valid) {
      throw new Error(reservedResult.error!.message);
    }

    // NOTE: Conflict checks removed per Section 5.2 of api-refactoring-final.md
    // Aliases now use override behavior - new requests take priority over existing aliases
    // Flow alias conflicts are handled by JSONB field overwrite (no check needed)
  }

  // DEPRECATED: Removed per Section 5.2 - aliases now use override behavior
  // private async checkProjectAliasConflict(alias: string, projectId: string): Promise<void> {
  //   const existing = await db.query.images.findFirst({
  //     where: and(
  //       eq(images.projectId, projectId),
  //       eq(images.alias, alias),
  //       isNull(images.deletedAt),
  //       isNull(images.flowId)
  //     ),
  //   });
  //
  //   if (existing) {
  //     throw new Error(ERROR_MESSAGES.ALIAS_CONFLICT);
  //   }
  // }

  // DEPRECATED: Removed per Section 5.2 - flow aliases now use override behavior
  // Flow alias conflicts are naturally handled by JSONB field overwrite in assignFlowAlias()
  // private async checkFlowAliasConflict(alias: string, flowId: string, projectId: string): Promise<void> {
  //   const flow = await db.query.flows.findFirst({
  //     where: and(eq(flows.id, flowId), eq(flows.projectId, projectId)),
  //   });
  //
  //   if (!flow) {
  //     throw new Error(ERROR_MESSAGES.FLOW_NOT_FOUND);
  //   }
  //
  //   const flowAliases = flow.aliases as Record<string, string>;
  //   if (flowAliases[alias]) {
  //     throw new Error(ERROR_MESSAGES.ALIAS_CONFLICT);
  //   }
  // }

  async resolveMultiple(
    aliases: string[],
    projectId: string,
    flowId?: string
  ): Promise<Map<string, AliasResolution>> {
    const resolutions = new Map<string, AliasResolution>();

    for (const alias of aliases) {
      const resolution = await this.resolve(alias, projectId, flowId);
      if (resolution) {
        resolutions.set(alias, resolution);
      }
    }

    return resolutions;
  }

  async resolveToImageIds(
    aliases: string[],
    projectId: string,
    flowId?: string
  ): Promise<string[]> {
    const imageIds: string[] = [];

    for (const alias of aliases) {
      const resolution = await this.resolve(alias, projectId, flowId);
      if (resolution) {
        imageIds.push(resolution.imageId);
      } else {
        throw new Error(`${ERROR_MESSAGES.ALIAS_NOT_FOUND}: ${alias}`);
      }
    }

    return imageIds;
  }
}
