import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';

import { PrismaService } from '@/prisma/prisma.service';

import { CreateTranslationDto } from './dto/create-translation.dto';
import {
  TranslationDictionaryQueryDto,
  TranslationKeyQueryDto,
  TranslationQueryDto,
} from './dto/translation-query.dto';
import { UpdateTranslationDto } from './dto/update-translation.dto';

// ============================================================
// LOCAL TYPES
// These local types keep strict ESLint/TypeScript happy even when
// Prisma's generated delegate types are not resolved by the editor.
// ============================================================

type TranslationEntity = {
  id: number;
  translationKey: string;
  englishText: string;
  khmerText: string;
  module: string | null;
  page: string | null;
  parentId: number | null;
  isActive: boolean;
  createdAt: Date;
  updatedAt: Date;
};

type StringFilter = {
  contains?: string;
  not?: null;
};

type TranslationSearchCondition = {
  translationKey?: StringFilter;
  englishText?: StringFilter;
  khmerText?: StringFilter;
  module?: StringFilter;
  page?: StringFilter;
};

type TranslationWhere = {
  id?: number;
  translationKey?: string;
  isActive?: boolean;
  module?: string | StringFilter;
  page?: string | StringFilter;
  parentId?: number | null;
  OR?: TranslationSearchCondition[];
};

type TranslationCreateData = {
  translationKey: string;
  englishText: string;
  khmerText: string;
  module: string;
  page: string;
  parentId: number | null;
  isActive: boolean;
};

type TranslationUpdateData = {
  translationKey?: string;
  englishText?: string;
  khmerText?: string;
  module?: string;
  page?: string;
  parentId?: number | null;
  isActive?: boolean;
};

type TranslationOrderBy = {
  id?: 'asc' | 'desc';
  translationKey?: 'asc' | 'desc';
  module?: 'asc' | 'desc';
  page?: 'asc' | 'desc';
};

type TranslationFindManyArgs = {
  where?: TranslationWhere;
  skip?: number;
  take?: number;
  orderBy?: TranslationOrderBy;
};

type TranslationCountArgs = {
  where?: TranslationWhere;
};

type CompositeTranslationUnique = {
  module: string;
  page: string;
  translationKey: string;
};

type TranslationDelegate = {
  findUnique(args: {
    where:
      | {
          id: number;
        }
      | {
          module_page_translationKey: CompositeTranslationUnique;
        };
  }): Promise<TranslationEntity | null>;

  findFirst(args: {
    where: TranslationWhere;
    orderBy?: TranslationOrderBy;
  }): Promise<TranslationEntity | null>;

  findMany(args?: TranslationFindManyArgs): Promise<TranslationEntity[]>;

  count(args?: TranslationCountArgs): Promise<number>;

  create(args: { data: TranslationCreateData }): Promise<TranslationEntity>;

  update(args: {
    where: {
      id: number;
    };
    data: TranslationUpdateData;
  }): Promise<TranslationEntity>;

  delete(args: {
    where: {
      id: number;
    };
  }): Promise<TranslationEntity>;
};

type TranslationDatabase = {
  translation: TranslationDelegate;
};

type PageOption = {
  id: number;
  page: string;
  translationKey?: string;
  nameEn: string;
  nameKh: string;
  parentId: number | null;
};

// ============================================================
// HELPERS
// ============================================================

function normalizeCode(value: string): string {
  return value.trim().toLowerCase();
}

function pageKeyToTitle(value: string): string {
  return value
    .split(/[_-]+/)
    .filter(Boolean)
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join(' ');
}

function splitLegacyTranslationKey(translationKey: string): {
  page: string | null;
  key: string;
} {
  const dotIndex = translationKey.indexOf('.');

  if (dotIndex <= 0) {
    return {
      page: null,
      key: translationKey,
    };
  }

  const page = translationKey.slice(0, dotIndex).trim();

  const key = translationKey.slice(dotIndex + 1).trim();

  if (!page || !key) {
    return {
      page: null,
      key: translationKey,
    };
  }

  return {
    page,
    key,
  };
}

function getEffectiveTranslationParts(item: TranslationEntity): {
  page: string | null;
  key: string;
} {
  if (item.page) {
    return {
      page: item.page,
      key: item.translationKey,
    };
  }

  return splitLegacyTranslationKey(item.translationKey);
}

function getBulkErrorMessage(error: unknown): string {
  if (error instanceof Error) {
    return error.message;
  }

  return 'Unable to create translation.';
}

// ============================================================
// SERVICE
// ============================================================

@Injectable()
export class TranslationsService {
  private readonly db: TranslationDatabase;

  constructor(prisma: PrismaService) {
    this.db = prisma as unknown as TranslationDatabase;
  }

  // ============================================================
  // CREATE
  // ============================================================

  async create(dto: CreateTranslationDto) {
    const translationKey = dto.translationKey.trim();
    const englishText = dto.englishText.trim();
    const khmerText = dto.khmerText.trim();
    const module = normalizeCode(dto.module);
    const page = normalizeCode(dto.page);

    if (!translationKey) {
      throw new BadRequestException('Translation key is required.');
    }

    if (!englishText) {
      throw new BadRequestException('English text is required.');
    }

    if (!khmerText) {
      throw new BadRequestException('Khmer text is required.');
    }

    if (!module) {
      throw new BadRequestException('Module is required.');
    }

    if (!page) {
      throw new BadRequestException('Page is required.');
    }

    const existing = await this.db.translation.findUnique({
      where: {
        module_page_translationKey: {
          module,
          page,
          translationKey,
        },
      },
    });

    if (existing) {
      throw new ConflictException(
        `Translation key "${translationKey}" already exists in module "${module}" page "${page}".`,
      );
    }

    const parentId = await this.resolveParentId({
      requestedParentId: dto.parentId,
      module,
      page,
    });

    const translation = await this.db.translation.create({
      data: {
        translationKey,
        englishText,
        khmerText,
        module,
        page,
        parentId,
        isActive: dto.isActive ?? true,
      },
    });

    return {
      success: true,
      message: 'Translation created successfully.',
      data: translation,
    };
  }

  // ============================================================
  // CREATE BULK
  // ============================================================

  async createBulk(dtos: CreateTranslationDto[]) {
    if (!dtos.length) {
      throw new BadRequestException('At least one translation is required.');
    }

    const created: TranslationEntity[] = [];

    const failed: Array<{
      index: number;
      translationKey: string;
      module: string;
      page: string;
      message: string;
    }> = [];

    for (let index = 0; index < dtos.length; index += 1) {
      const dto = dtos[index];

      try {
        const result = await this.create(dto);

        created.push(result.data);
      } catch (error) {
        failed.push({
          index,
          translationKey: dto.translationKey,
          module: dto.module,
          page: dto.page,
          message: getBulkErrorMessage(error),
        });
      }
    }

    return {
      success: failed.length === 0,
      message:
        failed.length === 0
          ? `${created.length} translations created successfully.`
          : `${created.length} translations created, ${failed.length} failed.`,
      data: {
        total: dtos.length,
        createdCount: created.length,
        failedCount: failed.length,
        created,
        failed,
      },
    };
  }

  // ============================================================
  // GET ALL
  // ============================================================

  async findAll(query: TranslationQueryDto) {
    const pageNumber = query.pageNumber ?? 1;

    const limit = query.limit ?? 10;

    const skip = (pageNumber - 1) * limit;

    const search = query.search?.trim();

    const moduleName = query.module ? normalizeCode(query.module) : undefined;

    const pageKey = query.pageKey ? normalizeCode(query.pageKey) : undefined;

    const where: TranslationWhere = {};

    if (query.isActive !== undefined) {
      where.isActive = query.isActive;
    }

    if (moduleName) {
      where.module = moduleName;
    }

    if (pageKey) {
      where.page = pageKey;
    }

    if (search) {
      where.OR = [
        {
          translationKey: {
            contains: search,
          },
        },
        {
          englishText: {
            contains: search,
          },
        },
        {
          khmerText: {
            contains: search,
          },
        },
        {
          module: {
            contains: search,
          },
        },
        {
          page: {
            contains: search,
          },
        },
      ];
    }

    const items = await this.db.translation.findMany({
      where,
      skip,
      take: limit,
      orderBy: {
        id: 'desc',
      },
    });

    const total = await this.db.translation.count({
      where,
    });

    const totalPages = Math.ceil(total / limit);

    return {
      success: true,
      data: {
        items,
        meta: {
          page: pageNumber,
          limit,
          total,
          totalPages,
          hasNextPage: pageNumber < totalPages,
          hasPreviousPage: pageNumber > 1,
        },
      },
    };
  }

  // ============================================================
  // GET PAGES BY MODULE
  // Dynamic: data comes from translations.page in the database.
  // Page display names use the "title" translation when available.
  // ============================================================

  async getPages(moduleValue: string) {
    const module = normalizeCode(moduleValue);

    if (!module) {
      throw new BadRequestException('Module is required.');
    }

    const rows = await this.db.translation.findMany({
      where: {
        module,
      },
      orderBy: {
        id: 'asc',
      },
    });

    const groupedRows = new Map<string, TranslationEntity[]>();

    for (const row of rows) {
      const parts = getEffectiveTranslationParts(row);

      if (!parts.page) {
        continue;
      }

      const groupKey =
        parts.page === 'sidebar'
          ? `${parts.page}::${row.translationKey}`
          : parts.page;

      const current = groupedRows.get(groupKey) ?? [];
      current.push(row);
      groupedRows.set(groupKey, current);
    }

    const items: PageOption[] = [];

    for (const pageRows of groupedRows.values()) {
      const first = pageRows[0];

      if (!first) {
        continue;
      }

      const parts = getEffectiveTranslationParts(first);

      if (!parts.page) {
        continue;
      }

      const representative =
        parts.page === 'sidebar'
          ? first
          : (pageRows.find(
              (row) => getEffectiveTranslationParts(row).key === 'title',
            ) ??
            pageRows.find(
              (row) => getEffectiveTranslationParts(row).key === 'subtitle',
            ) ??
            first);

      items.push({
        id: representative.id,
        page: parts.page,
        translationKey:
          parts.page === 'sidebar' ? representative.translationKey : undefined,
        nameEn: representative.englishText || pageKeyToTitle(parts.page),
        nameKh: representative.khmerText || pageKeyToTitle(parts.page),
        parentId: representative.parentId,
      });
    }

    return {
      success: true,
      data: {
        module,
        items: items.sort((a, b) => a.nameEn.localeCompare(b.nameEn)),
      },
    };
  }

  // ============================================================
  // GET ONE
  // ============================================================

  async findOne(id: number) {
    const translation = await this.db.translation.findUnique({
      where: {
        id,
      },
    });

    if (!translation) {
      throw new NotFoundException(`Translation with ID ${id} not found.`);
    }

    return {
      success: true,
      data: translation,
    };
  }

  // ============================================================
  // GET BY KEY
  // Because translationKey is no longer globally unique,
  // module + page should normally be supplied.
  // ============================================================

  async findByKey(translationKeyValue: string, query: TranslationKeyQueryDto) {
    const translationKey = translationKeyValue.trim();

    if (!translationKey) {
      throw new BadRequestException('Translation key is required.');
    }

    const module = query.module ? normalizeCode(query.module) : undefined;

    const page = query.page ? normalizeCode(query.page) : undefined;

    let translation = await this.db.translation.findFirst({
      where: {
        translationKey,
        ...(module
          ? {
              module,
            }
          : {}),
        ...(page
          ? {
              page,
            }
          : {}),
      },
      orderBy: {
        id: 'desc',
      },
    });

    // Backward compatibility:
    // old records stored "meeting_requests.title" in translationKey
    // and had page = null.
    if (!translation && page) {
      translation = await this.db.translation.findFirst({
        where: {
          translationKey: `${page}.${translationKey}`,
          ...(module
            ? {
                module,
              }
            : {}),
        },
        orderBy: {
          id: 'desc',
        },
      });
    }

    if (!translation) {
      throw new NotFoundException(
        `Translation key "${translationKey}" not found.`,
      );
    }

    return {
      success: true,
      data: translation,
    };
  }

  // ============================================================
  // GET DICTIONARY
  // Recommended page usage:
  // /translations/dictionary?language=en&module=pswg&page=meeting_requests
  // ============================================================

  async getDictionary(query: TranslationDictionaryQueryDto) {
    const language = query.language ?? 'en';

    const module = query.module ? normalizeCode(query.module) : undefined;

    const page = query.page ? normalizeCode(query.page) : undefined;

    const where: TranslationWhere = {
      isActive: true,
    };

    if (module) {
      where.module = module;
    }

    // Do not filter by DB page here because legacy rows may have page = null.
    const items = await this.db.translation.findMany({
      where,
      orderBy: {
        translationKey: 'asc',
      },
    });

    const translations: Record<string, string> = {};

    for (const item of items) {
      const parts = getEffectiveTranslationParts(item);

      if (page && parts.page !== page) {
        continue;
      }

      const key = page
        ? parts.key
        : parts.page
          ? `${parts.page}.${parts.key}`
          : parts.key;

      translations[key] = language === 'km' ? item.khmerText : item.englishText;
    }

    return {
      success: true,
      data: {
        language,
        module: module ?? null,
        page: page ?? null,
        translations,
      },
    };
  }

  async getEnglishDictionary(module?: string, page?: string) {
    return this.getDictionary({
      language: 'en',
      module,
      page,
    });
  }

  async getKhmerDictionary(module?: string, page?: string) {
    return this.getDictionary({
      language: 'km',
      module,
      page,
    });
  }

  // ============================================================
  // GET MODULES
  // ============================================================

  async getModules() {
    const rows = await this.db.translation.findMany({
      where: {
        module: {
          not: null,
        },
      },
      orderBy: {
        module: 'asc',
      },
    });

    const modules: string[] = [];

    for (const row of rows) {
      if (row.module && !modules.includes(row.module)) {
        modules.push(row.module);
      }
    }

    return {
      success: true,
      data: {
        items: modules,
      },
    };
  }

  // ============================================================
  // UPDATE
  // ============================================================

  async update(id: number, dto: UpdateTranslationDto) {
    const current = await this.getOrThrow(id);

    const nextTranslationKey =
      dto.translationKey !== undefined
        ? dto.translationKey.trim()
        : current.translationKey;

    const nextModule =
      dto.module !== undefined ? normalizeCode(dto.module) : current.module;

    const nextPage =
      dto.page !== undefined ? normalizeCode(dto.page) : current.page;

    if (!nextTranslationKey) {
      throw new BadRequestException('Translation key is required.');
    }

    if (!nextModule) {
      throw new BadRequestException('Module is required.');
    }

    if (!nextPage) {
      throw new BadRequestException('Page is required.');
    }

    const duplicate = await this.db.translation.findUnique({
      where: {
        module_page_translationKey: {
          module: nextModule,
          page: nextPage,
          translationKey: nextTranslationKey,
        },
      },
    });

    if (duplicate && duplicate.id !== id) {
      throw new ConflictException(
        `Translation key "${nextTranslationKey}" already exists in module "${nextModule}" page "${nextPage}".`,
      );
    }

    const nextParentId =
      dto.parentId !== undefined
        ? await this.resolveParentId({
            requestedParentId: dto.parentId,
            module: nextModule,
            page: nextPage,
            currentId: id,
          })
        : current.parentId;

    if (nextParentId !== null) {
      await this.validateParent(nextParentId, nextModule, id);
    }

    const data: TranslationUpdateData = {};

    if (dto.translationKey !== undefined) {
      data.translationKey = nextTranslationKey;
    }

    if (dto.englishText !== undefined) {
      const englishText = dto.englishText.trim();

      if (!englishText) {
        throw new BadRequestException('English text is required.');
      }

      data.englishText = englishText;
    }

    if (dto.khmerText !== undefined) {
      const khmerText = dto.khmerText.trim();

      if (!khmerText) {
        throw new BadRequestException('Khmer text is required.');
      }

      data.khmerText = khmerText;
    }

    if (dto.module !== undefined) {
      data.module = nextModule;
    }

    if (dto.page !== undefined) {
      data.page = nextPage;
    }

    if (dto.parentId !== undefined) {
      data.parentId = nextParentId;
    }

    if (dto.isActive !== undefined) {
      data.isActive = dto.isActive;
    }

    const translation = await this.db.translation.update({
      where: {
        id,
      },
      data,
    });

    return {
      success: true,
      message: 'Translation updated successfully.',
      data: translation,
    };
  }

  // ============================================================
  // ENABLE
  // ============================================================

  async enable(id: number) {
    await this.getOrThrow(id);

    const translation = await this.db.translation.update({
      where: {
        id,
      },
      data: {
        isActive: true,
      },
    });

    return {
      success: true,
      message: 'Translation enabled successfully.',
      data: translation,
    };
  }

  // ============================================================
  // DELETE - HARD DELETE
  // ============================================================

  async remove(id: number) {
    await this.getOrThrow(id);

    const translation = await this.db.translation.delete({
      where: {
        id,
      },
    });

    return {
      success: true,
      message: 'Translation deleted successfully.',
      data: translation,
    };
  }

  // ============================================================
  // PRIVATE
  // ============================================================

  private async resolveParentId(options: {
    requestedParentId: number | null | undefined;
    module: string;
    page: string;
    currentId?: number;
  }): Promise<number | null> {
    const { requestedParentId, module, page, currentId } = options;

    if (requestedParentId === null) {
      return null;
    }

    if (requestedParentId !== undefined) {
      await this.validateParent(requestedParentId, module, currentId);
      return requestedParentId;
    }

    // Existing labels inside the same Page automatically inherit the
    // Page's parent. This keeps Add Label working without changing its flow.
    const existingPageRow = await this.db.translation.findFirst({
      where: {
        module,
        page,
      },
      orderBy: {
        id: 'asc',
      },
    });

    if (existingPageRow && existingPageRow.id !== currentId) {
      return existingPageRow.parentId;
    }

    return null;
  }

  private async validateParent(
    parentId: number,
    module: string,
    currentId?: number,
  ): Promise<void> {
    if (currentId !== undefined && parentId === currentId) {
      throw new BadRequestException('A Page cannot be its own parent.');
    }

    const parent = await this.db.translation.findUnique({
      where: {
        id: parentId,
      },
    });

    if (!parent) {
      throw new BadRequestException(
        `Parent Page with ID ${parentId} not found.`,
      );
    }

    if (parent.module !== module) {
      throw new BadRequestException(
        'Parent Page must belong to the same module.',
      );
    }

    if (currentId === undefined) {
      return;
    }

    // Protect against A -> B -> A cycles.
    let cursor: TranslationEntity | null = parent;
    let depth = 0;

    while (cursor?.parentId !== null && cursor?.parentId !== undefined) {
      if (cursor.parentId === currentId) {
        throw new BadRequestException(
          'Circular Page hierarchy is not allowed.',
        );
      }

      cursor = await this.db.translation.findUnique({
        where: {
          id: cursor.parentId,
        },
      });

      depth += 1;

      if (depth > 100) {
        throw new BadRequestException('Page hierarchy is too deep.');
      }
    }
  }

  private async getOrThrow(id: number): Promise<TranslationEntity> {
    const translation = await this.db.translation.findUnique({
      where: {
        id,
      },
    });

    if (!translation) {
      throw new NotFoundException(`Translation with ID ${id} not found.`);
    }

    return translation;
  }
}
