import {
  Injectable,
  NotFoundException,
  Logger,
  Inject,
  forwardRef,
  Optional,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Article, ArticleStatus } from './entities';
import { StrapiService } from './strapi.service';
import {
  CreateArticleDto,
  UpdateArticleDto,
  FindArticlesDto,
} from './articles.dto';
import { PushService } from './push/push.service';

@Injectable()
export class ArticlesService {
  private readonly logger = new Logger(ArticlesService.name);

  constructor(
    @InjectRepository(Article)
    private readonly articleRepository: Repository<Article>,
    @Inject(forwardRef(() => StrapiService))
    private readonly strapiService: StrapiService,
    @Optional()
    @Inject(forwardRef(() => PushService))
    private readonly pushService?: PushService,
  ) {}

  async create(dto: CreateArticleDto): Promise<Article> {
    const article = this.articleRepository.create({
      ...dto,
      status: dto.status || ArticleStatus.DRAFT,
    });
    return await this.articleRepository.save(article);
  }

  async findAll(
    dto: FindArticlesDto,
  ): Promise<{ data: Article[]; total: number }> {
    const { category, author, tag, status, search, page = 1, limit = 10 } = dto;

    const queryBuilder = this.articleRepository
      .createQueryBuilder('article')
      .leftJoinAndSelect('article.author', 'author')
      .leftJoinAndSelect('article.category', 'category');

    // Handle status filter - can be single value or comma-separated list
    if (status) {
      if (typeof status === 'string' && status.includes(',')) {
        const statuses = status.split(',').map((s) => s.trim());
        queryBuilder.where('article.status IN (:...statuses)', { statuses });
      } else {
        queryBuilder.where('article.status = :status', { status });
      }
    }
    // If no status specified, return all articles (for admin dashboard)

    if (category) {
      queryBuilder.andWhere('category.slug = :category', { category });
    }

    if (author) {
      queryBuilder.andWhere('author.slug = :author', { author });
    }

    if (tag) {
      queryBuilder.andWhere(':tag = ANY(article.tags)', { tag });
    }

    if (search) {
      queryBuilder.andWhere(
        '(article.title ILIKE :search OR article.content ILIKE :search)',
        { search: `%${search}%` },
      );
    }

    const [data, total] = await queryBuilder
      .orderBy('article.createdAt', 'DESC')
      .skip((page - 1) * limit)
      .take(limit)
      .getManyAndCount();

    return { data, total };
  }

  async findOne(id: string): Promise<Article> {
    const article = await this.articleRepository.findOne({
      where: { id },
      relations: ['author', 'category'],
    });

    if (!article) {
      throw new NotFoundException(`Article with ID ${id} not found`);
    }

    // Increment view count
    await this.articleRepository.increment({ id }, 'views', 1);

    return article;
  }

  async findBySlug(slug: string): Promise<Article> {
    const article = await this.articleRepository.findOne({
      where: { slug },
      relations: ['author', 'category'],
    });

    if (!article) {
      throw new NotFoundException(`Article with slug ${slug} not found`);
    }

    // Increment view count
    await this.articleRepository.increment({ slug }, 'views', 1);

    return article;
  }

  async findOneWithoutIncrement(id: string): Promise<Article> {
    const article = await this.articleRepository.findOne({
      where: { id },
      relations: ['author', 'category'],
    });

    if (!article) {
      throw new NotFoundException(`Article with ID ${id} not found`);
    }

    return article;
  }

  async update(id: string, dto: UpdateArticleDto): Promise<Article> {
    const article = await this.findOneWithoutIncrement(id);
    const oldStatus = article.status;
    Object.assign(article, dto);
    const savedArticle = await this.articleRepository.save(article);

    // Update Strapi if status changed and article has strapiId
    if (
      dto.status !== undefined &&
      dto.status !== oldStatus &&
      article.strapiId
    ) {
      try {
        await this.strapiService.updateArticleStatusInStrapi(
          article.strapiId,
          dto.status,
        );
      } catch (error) {
        // Log error but don't fail - backend status is updated
        this.logger.error(
          `Failed to update Strapi status for article ${id}:`,
          error,
        );
      }
    }

    return savedArticle;
  }

  async remove(id: string): Promise<void> {
    const article = await this.findOneWithoutIncrement(id);

    // Delete from Strapi if article has strapiId
    if (article.strapiId) {
      try {
        await this.strapiService.deleteArticleFromStrapi(article.strapiId);
      } catch (error) {
        // Log error but don't fail - we still want to delete from backend
        this.logger.error(`Failed to delete article ${id} from Strapi:`, error);
      }
    }

    await this.articleRepository.remove(article);
  }

  async archive(id: string): Promise<Article> {
    const article = await this.findOneWithoutIncrement(id);
    article.status = ArticleStatus.ARCHIVED;
    const savedArticle = await this.articleRepository.save(article);

    // Update Strapi if article has strapiId
    if (article.strapiId) {
      try {
        await this.strapiService.updateArticleStatusInStrapi(
          article.strapiId,
          ArticleStatus.ARCHIVED,
        );
      } catch (error) {
        // Log error but don't fail - backend status is updated
        this.logger.error(
          `Failed to update Strapi status for article ${id}:`,
          error,
        );
      }
    }

    return savedArticle;
  }

  async publish(
    id: string,
    status: ArticleStatus = ArticleStatus.PUBLISHED,
  ): Promise<Article> {
    const article = await this.findOneWithoutIncrement(id);
    const wasDraft = article.status === ArticleStatus.DRAFT;
    article.status = status;
    const savedArticle = await this.articleRepository.save(article);

    // Update Strapi if article has strapiId
    if (article.strapiId) {
      try {
        await this.strapiService.updateArticleStatusInStrapi(
          article.strapiId,
          status,
        );
      } catch (error) {
        // Log error but don't fail - backend status is updated
        this.logger.error(
          `Failed to update Strapi status for article ${id}:`,
          error,
        );
      }
    }

    // Send push notification for newly published articles
    if (wasDraft && status === ArticleStatus.PUBLISHED && this.pushService) {
      try {
        await this.pushService.notifyNewArticle(
          savedArticle.title,
          savedArticle.slug,
        );
        this.logger.log(
          `Push notification sent for article: ${savedArticle.title}`,
        );
      } catch (error) {
        this.logger.error('Failed to send push notification:', error);
      }
    }

    return savedArticle;
  }

  async syncFromStrapi(
    strapiId: string,
    data: Partial<CreateArticleDto>,
  ): Promise<Article> {
    // Use upsert to handle race conditions and ensure uniqueness
    try {
      // First try to find existing article by strapiId
      const article = await this.articleRepository.findOne({
        where: { strapiId },
      });

      if (article) {
        // Update existing article
        const currentStatus = article.status;
        Object.assign(article, data);

        // Preserve archived status if article is already archived in our system
        // unless Strapi explicitly sends a different status
        if (
          currentStatus === ArticleStatus.ARCHIVED &&
          data.status !== ArticleStatus.ARCHIVED
        ) {
          article.status = ArticleStatus.ARCHIVED;
        }

        return await this.articleRepository.save(article);
      } else {
        // Create new article
        const newArticle = this.articleRepository.create({
          strapiId,
          ...data,
          status: data.status || ArticleStatus.DRAFT,
        });

        return await this.articleRepository.save(newArticle);
      }
    } catch (error: unknown) {
      // If we get a unique constraint violation, try to find and update again
      // This handles race conditions where two syncs happen simultaneously
      const dbError = error as { code?: string; constraint?: string };
      if (
        dbError.code === '23505' &&
        dbError.constraint?.includes('strapiId')
      ) {
        this.logger.warn(
          `Race condition detected for strapiId ${strapiId}, retrying...`,
        );

        // Wait a bit and retry
        await new Promise((resolve) => setTimeout(resolve, 100));

        const existingArticle = await this.articleRepository.findOne({
          where: { strapiId },
        });

        if (existingArticle) {
          const currentStatus = existingArticle.status;
          Object.assign(existingArticle, data);

          if (
            currentStatus === ArticleStatus.ARCHIVED &&
            data.status !== ArticleStatus.ARCHIVED
          ) {
            existingArticle.status = ArticleStatus.ARCHIVED;
          }

          return await this.articleRepository.save(existingArticle);
        }
      }

      // Re-throw other errors
      throw error;
    }
  }

  async removeByStrapiId(strapiId: string): Promise<void> {
    const article = await this.articleRepository.findOne({
      where: { strapiId },
    });

    if (!article) {
      this.logger.warn(`Article with strapiId ${strapiId} not found`);
      return;
    }

    await this.articleRepository.remove(article);
    this.logger.log(`Successfully deleted article with strapiId: ${strapiId}`);
  }

  async findHeroArticle(): Promise<Article | null> {
    return this.articleRepository.findOne({
      where: {
        isHero: true,
        status: ArticleStatus.PUBLISHED,
      },
      relations: ['author', 'category'],
      order: { createdAt: 'DESC' },
    });
  }
}
