import { EditorMode, MkdocsExportMode, Prisma } from '@prisma/client';
import fs from 'fs/promises';
import path from 'path';
import { prisma } from '../../config/database';
import { env } from '../../config/env';
import { AppError } from '../../middleware/error-handler';
import { logger } from '../../utils/logger';
import type { CreateLandingPageInput, UpdateLandingPageInput, ListLandingPagesInput } from './pages.schemas';

const landingPageSelect = {
  id: true,
  slug: true,
  title: true,
  description: true,
  editorMode: true,
  blocks: true,
  htmlOutput: true,
  cssOutput: true,
  mkdocsPath: true,
  mkdocsStubPath: true,
  mkdocsExportMode: true,
  mkdocsHideNav: true,
  mkdocsHideToc: true,
  mkdocsSkipExport: true,
  published: true,
  listed: true,
  seoTitle: true,
  seoDescription: true,
  seoImage: true,
  createdAt: true,
  updatedAt: true,
} satisfies Prisma.LandingPageSelect;

function generateSlug(title: string): string {
  return title
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 80);
}

async function resolveSlugCollision(slug: string, excludeId?: string): Promise<string> {
  let candidate = slug;
  let suffix = 2;

  while (true) {
    const existing = await prisma.landingPage.findUnique({
      where: { slug: candidate },
      select: { id: true },
    });

    if (!existing || (excludeId && existing.id === excludeId)) {
      return candidate;
    }

    candidate = `${slug}-${suffix}`;
    suffix++;
  }
}

const MKDOCS_OVERRIDES = path.join(env.MKDOCS_DOCS_PATH, 'overrides');
const MKDOCS_DOCS_ROOT = env.MKDOCS_DOCS_PATH;

function validateMkdocsPath(mkdocsPath: string): void {
  // Check for null bytes
  if (mkdocsPath.includes('\0')) {
    throw new AppError(400, 'Invalid path: null byte detected', 'INVALID_MKDOCS_PATH');
  }

  // Normalize and check for traversal
  const normalized = path.normalize(mkdocsPath);
  if (normalized.includes('..') || path.isAbsolute(normalized)) {
    throw new AppError(400, 'Path traversal not allowed', 'INVALID_MKDOCS_PATH');
  }

  // Check for encoded traversal sequences
  if (mkdocsPath.includes('%2e') || mkdocsPath.includes('%2E')) {
    throw new AppError(400, 'Encoded path traversal not allowed', 'INVALID_MKDOCS_PATH');
  }

  if (!mkdocsPath.endsWith('.html')) {
    throw new AppError(400, 'Path must end with .html', 'INVALID_MKDOCS_PATH');
  }
}

function validateStubPath(stubPath: string): void {
  // Check for null bytes
  if (stubPath.includes('\0')) {
    throw new AppError(400, 'Invalid path: null byte detected', 'INVALID_STUB_PATH');
  }

  // Normalize and check for traversal
  const normalized = path.normalize(stubPath);
  if (normalized.includes('..') || path.isAbsolute(normalized)) {
    throw new AppError(400, 'Path traversal not allowed', 'INVALID_STUB_PATH');
  }

  // Check for encoded traversal sequences
  if (stubPath.includes('%2e') || stubPath.includes('%2E')) {
    throw new AppError(400, 'Encoded path traversal not allowed', 'INVALID_STUB_PATH');
  }
}

function wrapInMaterialOverride(html: string, css: string | null): string {
  const styleBlock = css ? `<style>\n${css}\n</style>` : '';
  // Wrap dynamic content in {% raw %} to prevent Jinja2 SSTI via admin-authored HTML/CSS
  return `{% extends "main.html" %}
{% block content %}
{% raw %}
${styleBlock}
${html}
{% endraw %}
{% endblock %}
`;
}

function wrapInStandaloneDocument(html: string, css: string | null, title: string, description: string | null): string {
  const metaDesc = description ? `\n    <meta name="description" content="${description.replace(/"/g, '&quot;')}">` : '';
  const styleBlock = css ? `\n    <style>\n${css}\n    </style>` : '';
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${title.replace(/</g, '&lt;')}</title>${metaDesc}${styleBlock}
</head>
<body>
${html}
</body>
</html>
`;
}

function deriveStubPath(overridePath: string): string {
  return overridePath.replace(/\.html$/, '.md');
}

interface StubOptions {
  overrideFilename: string;
  title: string;
  description: string | null;
  hideNav: boolean;
  hideToc: boolean;
}

function generateMdStub(opts: StubOptions): string {
  const hideItems: string[] = [];
  if (opts.hideNav) hideItems.push('  - navigation');
  if (opts.hideToc) hideItems.push('  - toc');

  const hideBlock = hideItems.length > 0 ? `hide:\n${hideItems.join('\n')}\n` : '';
  const descLine = opts.description ? `description: "${opts.description.replace(/"/g, '\\"')}"\n` : '';

  return `---
template: ${opts.overrideFilename}
${hideBlock}title: "${opts.title.replace(/"/g, '\\"')}"
${descLine}---
`;
}

async function writeStubFile(stubPath: string, content: string): Promise<void> {
  try {
    validateStubPath(stubPath);
    const filePath = path.join(MKDOCS_DOCS_ROOT, stubPath);
    await fs.mkdir(path.dirname(filePath), { recursive: true });
    await fs.writeFile(filePath, content, 'utf-8');
    logger.info(`Wrote MkDocs stub: ${stubPath}`);
  } catch (err) {
    if (err instanceof AppError) throw err;
    logger.warn(`Failed to write stub: ${stubPath}`, err);
  }
}

async function removeStubFile(stubPath: string): Promise<void> {
  try {
    const filePath = path.join(MKDOCS_DOCS_ROOT, stubPath);
    await fs.unlink(filePath);
    logger.info(`Removed MkDocs stub: ${stubPath}`);
  } catch {
    // File may not exist — ignore
  }
}

interface ExportOptions {
  mkdocsPath: string;
  html: string;
  css: string | null;
  editorMode: EditorMode;
  exportMode: MkdocsExportMode;
  title: string;
  seoTitle: string | null;
  seoDescription: string | null;
  hideNav: boolean;
  hideToc: boolean;
}

async function exportToMkDocs(opts: ExportOptions): Promise<string> {
  const { mkdocsPath, html, css, editorMode, exportMode, title, seoTitle, seoDescription, hideNav, hideToc } = opts;

  try {
    validateMkdocsPath(mkdocsPath);

    // Write the override template
    const filePath = path.join(MKDOCS_OVERRIDES, mkdocsPath);
    await fs.mkdir(path.dirname(filePath), { recursive: true });

    let content: string;
    if (editorMode === 'CODE') {
      content = html;
    } else if (exportMode === 'STANDALONE') {
      content = wrapInStandaloneDocument(html, css, seoTitle || title, seoDescription);
    } else {
      content = wrapInMaterialOverride(html, css);
    }

    // Rewrite relative media/gallery URLs to absolute for MkDocs context
    const adminUrl = env.ADMIN_URL || 'http://localhost:3000';
    content = content.replace(/src="\/media\/public\//g, `src="${adminUrl}/media/public/`);
    content = content.replace(/href="\/gallery\/watch\//g, `href="${adminUrl}/gallery/watch/`);
    content = content.replace(/href="\/gallery\?expanded=/g, `href="${adminUrl}/gallery?expanded=`);
    content = content.replace(/src="http:\/\/localhost:4100\//g, `src="${adminUrl.replace(/:\d+$/, ':4100')}/`);

    // Rewrite payment page URLs to absolute for MkDocs context
    content = content.replace(/href="\/donate"/g, `href="${adminUrl}/donate"`);
    content = content.replace(/href="\/pricing"/g, `href="${adminUrl}/pricing"`);
    content = content.replace(/href="\/shop"/g, `href="${adminUrl}/shop"`);
    content = content.replace(/href="\/payments\/success"/g, `href="${adminUrl}/payments/success"`);

    await fs.writeFile(filePath, content, 'utf-8');
    logger.info(`Exported landing page to MkDocs: ${mkdocsPath} (${editorMode}/${exportMode})`);

    // Write the .md stub
    const stubPath = deriveStubPath(mkdocsPath);
    const stubContent = generateMdStub({
      overrideFilename: mkdocsPath,
      title: seoTitle || title,
      description: seoDescription,
      hideNav,
      hideToc,
    });
    await writeStubFile(stubPath, stubContent);

    return stubPath;
  } catch (err) {
    if (err instanceof AppError) throw err;
    logger.warn(`Failed to export to MkDocs: ${mkdocsPath}`, err);
    return deriveStubPath(mkdocsPath);
  }
}

async function removeFromMkDocs(mkdocsPath: string, stubPath?: string | null): Promise<void> {
  try {
    const filePath = path.join(MKDOCS_OVERRIDES, mkdocsPath);
    await fs.unlink(filePath);
    logger.info(`Removed MkDocs export: ${mkdocsPath}`);
  } catch {
    // File may not exist — ignore
  }

  // Also remove the stub
  const effectiveStubPath = stubPath || deriveStubPath(mkdocsPath);
  await removeStubFile(effectiveStubPath);
}

async function scanOverrideFiles(dir: string, base: string = ''): Promise<{ relativePath: string; fullPath: string }[]> {
  const results: { relativePath: string; fullPath: string }[] = [];
  try {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
      const rel = base ? `${base}/${entry.name}` : entry.name;
      const full = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        results.push(...await scanOverrideFiles(full, rel));
      } else if (entry.name.endsWith('.html')) {
        results.push({ relativePath: rel, fullPath: full });
      }
    }
  } catch {
    // Directory may not exist
  }
  return results;
}

async function stubExistsOnDisk(stubPath: string): Promise<boolean> {
  try {
    await fs.access(path.join(MKDOCS_DOCS_ROOT, stubPath));
    return true;
  } catch {
    return false;
  }
}

export const pagesService = {
  async findAll(filters: ListLandingPagesInput) {
    const { page, limit, search, published } = filters;
    const skip = (page - 1) * limit;

    const where: Prisma.LandingPageWhereInput = {};

    if (search) {
      where.OR = [
        { title: { contains: search, mode: 'insensitive' } },
        { description: { contains: search, mode: 'insensitive' } },
        { slug: { contains: search, mode: 'insensitive' } },
      ];
    }

    if (published === 'true') where.published = true;
    else if (published === 'false') where.published = false;

    const [pages, total] = await Promise.all([
      prisma.landingPage.findMany({
        where,
        select: landingPageSelect,
        skip,
        take: limit,
        orderBy: { createdAt: 'desc' },
      }),
      prisma.landingPage.count({ where }),
    ]);

    return {
      pages,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    };
  },

  async findById(id: string) {
    const page = await prisma.landingPage.findUnique({
      where: { id },
      select: landingPageSelect,
    });

    if (!page) {
      throw new AppError(404, 'Landing page not found', 'PAGE_NOT_FOUND');
    }

    return page;
  },

  async findBySlugPublic(slug: string) {
    const page = await prisma.landingPage.findUnique({
      where: { slug },
      select: landingPageSelect,
    });

    if (!page || !page.published) {
      throw new AppError(404, 'Page not found', 'PAGE_NOT_FOUND');
    }

    return page;
  },

  async create(data: CreateLandingPageInput) {
    const baseSlug = generateSlug(data.title);
    const slug = await resolveSlugCollision(baseSlug);

    // Auto-generate mkdocsPath from slug if not provided
    const mkdocsPath = data.mkdocsPath ?? `${slug}.html`;
    validateMkdocsPath(mkdocsPath);

    const page = await prisma.landingPage.create({
      data: {
        ...data,
        slug,
        mkdocsPath,
        blocks: data.blocks as unknown as Prisma.InputJsonValue,
      },
      select: landingPageSelect,
    });

    return page;
  },

  async update(id: string, data: UpdateLandingPageInput) {
    const existing = await prisma.landingPage.findUnique({ where: { id } });
    if (!existing) {
      throw new AppError(404, 'Landing page not found', 'PAGE_NOT_FOUND');
    }

    if (data.mkdocsPath) {
      validateMkdocsPath(data.mkdocsPath);
    }

    const updateData: Prisma.LandingPageUncheckedUpdateInput = { ...data };

    // Handle blocks JSON field
    if (data.blocks !== undefined) {
      updateData.blocks = data.blocks as unknown as Prisma.InputJsonValue;
    }

    // Regenerate slug if title changes, and keep mkdocsPath in sync
    if (data.title && data.title !== existing.title) {
      const baseSlug = generateSlug(data.title);
      const newSlug = await resolveSlugCollision(baseSlug, id);
      updateData.slug = newSlug;

      // Update mkdocsPath if it was auto-generated (matches old slug pattern)
      const autoPath = `${existing.slug}.html`;
      if (existing.mkdocsPath === autoPath) {
        const oldMkdocsPath = existing.mkdocsPath;
        updateData.mkdocsPath = `${newSlug}.html`;
        // Clean up old override + old stub
        await removeFromMkDocs(oldMkdocsPath, existing.mkdocsStubPath);
        updateData.mkdocsStubPath = null;
      }
    }

    const page = await prisma.landingPage.update({
      where: { id },
      data: updateData,
      select: landingPageSelect,
    });

    // Handle MkDocs export/cleanup based on publish state
    if (page.published && !page.mkdocsSkipExport && page.mkdocsPath && page.htmlOutput) {
      const stubPath = await exportToMkDocs({
        mkdocsPath: page.mkdocsPath,
        html: page.htmlOutput,
        css: page.cssOutput,
        editorMode: page.editorMode,
        exportMode: page.mkdocsExportMode,
        title: page.title,
        seoTitle: page.seoTitle,
        seoDescription: page.seoDescription,
        hideNav: page.mkdocsHideNav,
        hideToc: page.mkdocsHideToc,
      });

      // Store stubPath if changed
      if (stubPath !== page.mkdocsStubPath) {
        await prisma.landingPage.update({
          where: { id },
          data: { mkdocsStubPath: stubPath },
        });
      }
    } else if ((!page.published || page.mkdocsSkipExport) && existing.mkdocsPath) {
      await removeFromMkDocs(existing.mkdocsPath, existing.mkdocsStubPath);
      if (existing.mkdocsStubPath) {
        await prisma.landingPage.update({
          where: { id },
          data: { mkdocsStubPath: null },
        });
      }
    }

    return page;
  },

  async delete(id: string) {
    const existing = await prisma.landingPage.findUnique({ where: { id } });
    if (!existing) {
      throw new AppError(404, 'Landing page not found', 'PAGE_NOT_FOUND');
    }

    // Remove MkDocs export + stub if exists
    if (existing.mkdocsPath) {
      await removeFromMkDocs(existing.mkdocsPath, existing.mkdocsStubPath);
    }

    await prisma.landingPage.delete({ where: { id } });
  },

  async syncOverrides(): Promise<{ imported: number; updated: number; stubs: number }> {
    let imported = 0;
    let updated = 0;
    let stubs = 0;

    const files = await scanOverrideFiles(MKDOCS_OVERRIDES);
    if (files.length === 0) {
      logger.info('syncOverrides: No HTML files found in overrides directory');
    }

    // Get all existing pages with mkdocsPath set
    const existingPages = await prisma.landingPage.findMany({
      where: { mkdocsPath: { not: null } },
      select: {
        id: true,
        mkdocsPath: true,
        mkdocsStubPath: true,
        mkdocsExportMode: true,
        mkdocsHideNav: true,
        mkdocsHideToc: true,
        editorMode: true,
        title: true,
        seoTitle: true,
        seoDescription: true,
        published: true,
      },
    });

    const pathMap = new Map(existingPages.map(p => [p.mkdocsPath!, p]));

    for (const file of files) {
      const content = await fs.readFile(file.fullPath, 'utf-8');
      const tracked = pathMap.get(file.relativePath);

      if (!tracked) {
        // Untracked file — import as CODE page
        const title = path.basename(file.relativePath, '.html');
        const baseSlug = generateSlug(title);
        const slug = await resolveSlugCollision(baseSlug);

        await prisma.landingPage.create({
          data: {
            slug,
            title,
            editorMode: 'CODE',
            htmlOutput: content,
            mkdocsPath: file.relativePath,
            published: true,
            blocks: {} as unknown as Prisma.InputJsonValue,
          },
        });

        imported++;
        logger.info(`syncOverrides: Imported ${file.relativePath} as CODE page`);
      } else if (tracked.editorMode === 'CODE') {
        // Tracked CODE page — update from disk (disk wins)
        await prisma.landingPage.update({
          where: { id: tracked.id },
          data: { htmlOutput: content },
        });

        updated++;
        logger.info(`syncOverrides: Updated ${file.relativePath} from disk`);
      }
      // VISUAL pages: don't overwrite from disk (managed by GrapesJS)
    }

    // Backfill missing .md stubs for published pages with overrides
    for (const page of existingPages) {
      if (!page.published || !page.mkdocsPath) continue;

      const expectedStubPath = page.mkdocsStubPath || deriveStubPath(page.mkdocsPath);
      const exists = await stubExistsOnDisk(expectedStubPath);
      if (!exists) {
        const stubContent = generateMdStub({
          overrideFilename: page.mkdocsPath,
          title: page.seoTitle || page.title,
          description: page.seoDescription,
          hideNav: page.mkdocsHideNav,
          hideToc: page.mkdocsHideToc,
        });
        await writeStubFile(expectedStubPath, stubContent);

        if (page.mkdocsStubPath !== expectedStubPath) {
          await prisma.landingPage.update({
            where: { id: page.id },
            data: { mkdocsStubPath: expectedStubPath },
          });
        }

        stubs++;
        logger.info(`syncOverrides: Created missing stub ${expectedStubPath}`);
      }
    }

    logger.info(`syncOverrides: Imported ${imported}, updated ${updated}, stubs created ${stubs}`);
    return { imported, updated, stubs };
  },

  async validateExports(): Promise<{ validated: number; repaired: number; errors: Array<{ pageId: string; slug: string; error: string }> }> {
    let validated = 0;
    let repaired = 0;
    const errors: Array<{ pageId: string; slug: string; error: string }> = [];

    const pages = await prisma.landingPage.findMany({
      where: {
        published: true,
        mkdocsSkipExport: false,
        mkdocsPath: { not: null },
        htmlOutput: { not: null },
      },
      select: {
        id: true,
        slug: true,
        title: true,
        mkdocsPath: true,
        mkdocsStubPath: true,
        htmlOutput: true,
        cssOutput: true,
        editorMode: true,
        mkdocsExportMode: true,
        mkdocsHideNav: true,
        mkdocsHideToc: true,
        seoTitle: true,
        seoDescription: true,
      },
    });

    logger.info(`Validating MkDocs exports for ${pages.length} published pages`);

    for (const page of pages) {
      validated++;
      try {
        // Check override HTML exists
        const overridePath = path.join(MKDOCS_OVERRIDES, page.mkdocsPath!);
        let overrideExists = false;
        try {
          await fs.access(overridePath);
          overrideExists = true;
        } catch {
          // File doesn't exist
        }

        // Check stub exists
        const expectedStubPath = page.mkdocsStubPath || deriveStubPath(page.mkdocsPath!);
        const stubExists = await stubExistsOnDisk(expectedStubPath);

        // Repair if either missing
        if (!overrideExists || !stubExists) {
          logger.warn(`Missing exports for ${page.slug}: override=${overrideExists}, stub=${stubExists}. Re-exporting...`);

          const stubPath = await exportToMkDocs({
            mkdocsPath: page.mkdocsPath!,
            html: page.htmlOutput!,
            css: page.cssOutput,
            editorMode: page.editorMode,
            exportMode: page.mkdocsExportMode,
            title: page.title,
            seoTitle: page.seoTitle,
            seoDescription: page.seoDescription,
            hideNav: page.mkdocsHideNav,
            hideToc: page.mkdocsHideToc,
          });

          if (stubPath !== page.mkdocsStubPath) {
            await prisma.landingPage.update({
              where: { id: page.id },
              data: { mkdocsStubPath: stubPath },
            });
          }

          repaired++;
          logger.info(`✓ Repaired exports for ${page.slug}`);
        }
      } catch (err) {
        const errorMsg = err instanceof Error ? err.message : String(err);
        logger.error(`Failed to validate/repair ${page.slug}:`, err);
        errors.push({ pageId: page.id, slug: page.slug, error: errorMsg });
      }
    }

    logger.info(`Validation complete: ${validated} validated, ${repaired} repaired, ${errors.length} errors`);
    return { validated, repaired, errors };
  },
};
