import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { ContentBlock } from "./entities/content-block.entity";
import { Page } from "./entities/page.entity";
import { CreatePageDto } from "./dto/create-page.dto";
import { UpdateBlockDto } from "./dto/update-block.dto";
import { UpdateSeoDto } from "./dto/update-seo.dto";

// Reserved key prefix for per-page SEO/metadata fields (see
// upsertSeoFields below) — stored as ordinary content blocks so no schema
// change or separate table was needed, but kept out of the admin editor's
// generic block list (it renders its own dedicated SEO panel instead; see
// frontend/src/components/admin/SeoPanel.tsx) and sorted after real content
// if the two are ever listed together.
export const SEO_FIELDS = [
  { key: "seo.title", label: "SEO Title" },
  { key: "seo.description", label: "SEO Description" },
  { key: "seo.ogTitle", label: "Open Graph Title" },
  { key: "seo.ogDescription", label: "Open Graph Description" },
  { key: "seo.ogImage", label: "Open Graph Image", type: "image" as const },
] as const;

@Injectable()
export class ContentService {
  constructor(
    @InjectRepository(Page) private readonly pages: Repository<Page>,
    @InjectRepository(ContentBlock) private readonly blocks: Repository<ContentBlock>,
  ) {}

  listPages(): Promise<Page[]> {
    return this.pages.find({ order: { title: "ASC" } });
  }

  async getPageBySlug(slug: string): Promise<Page> {
    const page = await this.pages.findOne({
      where: { slug },
      relations: { blocks: true },
      order: { blocks: { sortOrder: "ASC" } },
    });
    if (!page) throw new NotFoundException(`No page with slug "${slug}"`);
    return page;
  }

  /**
   * Build-time content lookup used by the frontend's server components
   * (see frontend/src/lib/content-fetch.ts) — returns a plain
   * { key: value } map for a page, falling back to {} if the page doesn't
   * exist yet, so a page renders its hardcoded defaults until someone
   * seeds/edits real content for it.
   */
  async getPageContentMap(slug: string): Promise<Record<string, string>> {
    const page = await this.pages.findOne({ where: { slug }, relations: { blocks: true } });
    if (!page) return {};
    const map: Record<string, string> = {};
    for (const block of page.blocks) {
      map[block.key] = block.type === "image" ? (block.imageUrl ?? "") : (block.textValue ?? "");
    }
    return map;
  }

  async updateBlock(id: string, dto: UpdateBlockDto): Promise<ContentBlock> {
    const block = await this.blocks.findOne({ where: { id } });
    if (!block) throw new NotFoundException(`No content block with id "${id}"`);
    if (dto.textValue !== undefined) block.textValue = dto.textValue;
    if (dto.imageUrl !== undefined) block.imageUrl = dto.imageUrl;
    if (dto.type !== undefined) block.type = dto.type;
    return this.blocks.save(block);
  }

  /**
   * Upserts a page's 5 SEO/metadata fields (see SEO_FIELDS above) without
   * touching its regular content blocks — creates the Page row too if this
   * is the first time anything's been saved for this slug, so SEO can be
   * set on a page before its own content has ever been edited. Fields left
   * undefined in the dto are skipped (partial save from the admin panel).
   */
  async upsertSeoFields(slug: string, dto: UpdateSeoDto): Promise<ContentBlock[]> {
    let page = await this.pages.findOne({ where: { slug } });
    if (!page) {
      page = await this.pages.save(this.pages.create({ slug, title: slug }));
    }

    const values: Record<string, string | undefined> = {
      "seo.title": dto.title,
      "seo.description": dto.description,
      "seo.ogTitle": dto.ogTitle,
      "seo.ogDescription": dto.ogDescription,
      "seo.ogImage": dto.ogImage,
    };

    const results: ContentBlock[] = [];
    for (const field of SEO_FIELDS) {
      if (values[field.key] === undefined) continue;
      let block = await this.blocks.findOne({ where: { pageId: page.id, key: field.key } });
      if (!block) {
        block = this.blocks.create({
          pageId: page.id,
          key: field.key,
          label: field.label,
          type: "type" in field ? field.type : "text",
          sortOrder: 9000,
        });
      }
      const value = values[field.key] ?? "";
      if (block.type === "image") block.imageUrl = value || null;
      else block.textValue = value || null;
      results.push(await this.blocks.save(block));
    }
    return results;
  }

  /**
   * Creates a brand new page with its full set of blocks in one call —
   * used by the admin's "Add Post" flow (see /admin/dashboard/blog) to
   * create a new blog post without a code change/redeploy, since the
   * frontend's blog route is a single dynamic [slug] page that reads
   * whichever "blog/*" pages exist via generateStaticParams at build time.
   * Rejects a slug that's already taken instead of silently overwriting it
   * (unlike upsertPageWithBlocks, which is seed-script-only and meant to be
   * re-run safely).
   */
  async createPage(dto: CreatePageDto): Promise<Page> {
    const existing = await this.pages.findOne({ where: { slug: dto.slug } });
    if (existing) throw new BadRequestException(`A page with slug "${dto.slug}" already exists.`);

    const page = await this.pages.save(this.pages.create({ slug: dto.slug, title: dto.title }));
    for (let i = 0; i < dto.blocks.length; i++) {
      const b = dto.blocks[i];
      await this.blocks.save(
        this.blocks.create({
          pageId: page.id,
          key: b.key,
          label: b.label,
          type: b.type,
          sortOrder: i,
          textValue: b.type === "image" ? null : b.value,
          imageUrl: b.type === "image" ? b.value : null,
        }),
      );
    }
    return this.getPageBySlug(dto.slug);
  }

  // Blocks cascade-delete with the page (ContentBlock.page has
  // onDelete: "CASCADE") — see entities/content-block.entity.ts.
  async deletePage(slug: string): Promise<void> {
    const page = await this.pages.findOne({ where: { slug } });
    if (!page) throw new NotFoundException(`No page with slug "${slug}"`);
    await this.pages.remove(page);
  }

  /** Idempotent upsert used by seed scripts — safe to re-run. */
  async upsertPageWithBlocks(
    slug: string,
    title: string,
    blocks: { key: string; label: string; type: "text" | "richtext" | "image"; value: string }[],
  ): Promise<void> {
    let page = await this.pages.findOne({ where: { slug } });
    if (!page) {
      page = await this.pages.save(this.pages.create({ slug, title }));
    }

    const existing = await this.blocks.find({ where: { pageId: page.id } });
    const byKey = new Map(existing.map((b) => [b.key, b]));

    for (let i = 0; i < blocks.length; i++) {
      const b = blocks[i];
      const current = byKey.get(b.key);
      if (current) {
        current.label = b.label;
        current.sortOrder = i;
        await this.blocks.save(current);
      } else {
        await this.blocks.save(
          this.blocks.create({
            pageId: page.id,
            key: b.key,
            label: b.label,
            type: b.type,
            sortOrder: i,
            textValue: b.type === "image" ? null : b.value,
            imageUrl: b.type === "image" ? b.value : null,
          }),
        );
      }
    }
  }
}
