import { Hono } from 'hono';
import { createHash } from 'crypto';
import { ResourceRegistry, ResourceConflictError, ResourceNotFoundError } from '../services/registry.ts';
import { Cache } from '../services/cache.ts';
import { fetchEpub } from '../services/epub.ts';
import { fetchToc, fetchChapters } from '../services/scraper.ts';
import { renderHtml } from '../renderers/html.ts';
import { renderJson } from '../renderers/json.ts';
import type { AppConfig } from '../config/types.ts';

export function createNovelRouter(registry: ResourceRegistry, cache: Cache, appConfig: AppConfig) {
  const app = new Hono();

  // /available MUST be registered before /:novel — otherwise Hono matches "available" as an ID
  app.get('/available', c => {
    return c.json(registry.getAll());
  });

  app.get('/:novel', async c => {
    const novelParam = c.req.param('novel');
    const novelId = parseInt(novelParam, 10);
    if (isNaN(novelId)) {
      return c.json({ error: 'Invalid novel ID', detail: 'Novel ID must be a non-negative integer' }, 400);
    }

    const rangeString = c.req.query('r');
    if (!rangeString) {
      return c.json(
        { error: 'Missing required query parameter', detail: '?r=min,max is required, e.g. ?r=0,10' },
        400,
      );
    }

    const parts = rangeString.split(',');
    const min = parseInt(parts[0] ?? '', 10);
    const max = parseInt(parts[1] ?? '', 10);
    if (parts.length !== 2 || isNaN(min) || isNaN(max)) {
      return c.json(
        {
          error: 'Invalid range format',
          detail: `Expected ?r=min,max with integer values, got ?r=${rangeString}`,
        },
        400,
      );
    }
    if (min > max) {
      return c.json({ error: 'Invalid range', detail: 'min must be less than or equal to max' }, 400);
    }

    const format = c.req.query('f') ?? 'html';

    let resource;
    try {
      resource = registry.get(novelId);
    } catch (err) {
      if (err instanceof ResourceNotFoundError) {
        return c.json({ error: 'Unknown novel ID', detail: err.message }, 422);
      }
      throw err;
    }

    try {
      const view = await resource.fetch(min, max);
      if (format === 'json') {
        return c.body(renderJson(view), 200, { 'Content-Type': 'application/json' });
      }
      return c.html(renderHtml(view));
    } catch (err) {
      const detail = err instanceof Error ? err.message : String(err);
      console.error(`[route] Fetch failed for novel ${novelId} range ${min}-${max}: ${detail}`);
      return c.json({ error: 'Failed to fetch or render content', detail }, 500);
    }
  });

  app.post('/new', async c => {
    let body: { type?: unknown; slug?: unknown; urls?: unknown };
    try {
      body = await c.req.json<typeof body>();
    } catch {
      return c.json({ error: 'Invalid request body', detail: 'Body must be valid JSON' }, 400);
    }

    if (typeof body.type !== 'string' || !body.type) {
      return c.json({ error: 'Missing field', detail: '"type" is required and must be a string' }, 400);
    }
    if (typeof body.slug !== 'string' || !body.slug) {
      return c.json({ error: 'Missing field', detail: '"slug" is required and must be a string' }, 400);
    }
    if (!Array.isArray(body.urls) || body.urls.length === 0 || typeof body.urls[0] !== 'string') {
      return c.json(
        { error: 'Missing field', detail: '"urls" must be a non-empty array of strings' },
        400,
      );
    }

    const { type, slug } = body;
    const urls = body.urls as string[];

    if (type !== 'epub') {
      return c.json(
        {
          error: 'Unsupported type',
          detail: `Type "${type}" is not supported. Supported types: epub`,
        },
        400,
      );
    }

    const hash = createHash('sha256').update(type + urls.join(';')).digest('hex');
    const url = urls[0]!;

    let registeredId = -1;
    try {
      registeredId = registry.register({
        hash,
        slug,
        startsAt: 0,
        endsAt: -1,
        url,
        fetch: (from, to) =>
          fetchEpub(url, registeredId, from, to, cache, (start, end) =>
            registry.updateRange(registeredId, start, end),
          ),
      });
    } catch (err) {
      if (err instanceof ResourceConflictError) {
        return c.json(
          { error: 'Conflict', detail: 'A resource with those parameters is already registered' },
          409,
        );
      }
      const detail = err instanceof Error ? err.message : String(err);
      return c.json({ error: 'Registration failed', detail }, 500);
    }

    return c.text(String(registeredId));
  });

  return app;
}

export function registerNovelFromConfig(
  def: import('../config/types.ts').NovelDefinition,
  registry: ResourceRegistry,
  cache: Cache,
  appConfig: AppConfig,
): number {
  const sourceUrl = def.source.url ?? def.source.urls?.[0] ?? def.source.urlTemplate ?? '';
  const hash = createHash('sha256').update(def.slug + sourceUrl).digest('hex');

  let registeredId = -1;

  if (def.type === 'epub') {
    registeredId = registry.register({
      hash,
      slug: def.slug,
      startsAt: 0,
      endsAt: -1,
      url: sourceUrl,
      fetch: (from, to) =>
        fetchEpub(sourceUrl, registeredId, from, to, cache, (start, end) =>
          registry.updateRange(registeredId, start, end),
        ),
    });
  } else if (def.source.urlTemplate) {
    // Increment strategy: chapter URLs follow a predictable pattern — no TOC fetch needed.
    // {n} is replaced with the chapter number directly; endsAt is unknown so set to MAX_SAFE_INTEGER.
    const startIndex = def.source.startIndex ?? 1;
    const template = def.source.urlTemplate;
    registeredId = registry.register({
      hash,
      slug: def.slug,
      startsAt: startIndex,
      endsAt: Number.MAX_SAFE_INTEGER,
      url: template,
      fetch: async (from, to) => {
        const urls = Array.from(
          { length: to - from + 1 },
          (_, i) => template.replace('{n}', String(from + i)),
        );
        return fetchChapters(def, registeredId, from, urls, cache);
      },
    });
  } else {
    // TOC strategy: scrape the table-of-contents page to discover all chapter URLs.
    registeredId = registry.register({
      hash,
      slug: def.slug,
      startsAt: 0,
      endsAt: -1,
      url: sourceUrl,
      fetch: async (from, to) => {
        const toc = await fetchToc(def, registeredId, cache, appConfig);
        return fetchChapters(def, registeredId, from, toc.slice(from, to + 1), cache);
      },
    });
  }

  return registeredId;
}
