import { type Dirent, promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import yaml from 'js-yaml';
import { z } from 'zod';

import { resolveTemplate } from './templates.js';
import type { CreatePrdOptions, PrdDocument } from './types.js';

const PRD_DIRECTORY = path.join('docs', 'prdy');
const PRD_FILE_EXTENSIONS = new Set(['.yaml', '.yml']);

const prdSectionSchema = z.object({
  id: z.string().min(1),
  title: z.string().min(1),
  fields: z.record(z.string(), z.string()),
});

const prdDocumentSchema = z.object({
  id: z.string().min(1),
  title: z.string().min(1),
  status: z.enum(['draft', 'review', 'approved', 'archived']),
  projectPath: z.string().min(1),
  template: z.string().min(1),
  sections: z.array(prdSectionSchema),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});

function expandHome(projectPath: string): string {
  if (!projectPath.startsWith('~')) {
    return projectPath;
  }

  if (projectPath === '~') {
    return os.homedir();
  }

  if (projectPath.startsWith('~/')) {
    return path.join(os.homedir(), projectPath.slice(2));
  }

  return projectPath;
}

function resolveProjectPath(projectPath: string): string {
  return path.resolve(expandHome(projectPath));
}

function toSlug(value: string): string {
  return value
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .replace(/-{2,}/g, '-');
}

function buildTimestamp(date: Date): { datePart: string; timePart: string } {
  const iso = date.toISOString();
  return {
    datePart: iso.slice(0, 10).replace(/-/g, ''),
    timePart: iso.slice(11, 19).replace(/:/g, ''),
  };
}

function buildPrdId(name: string): string {
  const slug = toSlug(name);
  const { datePart, timePart } = buildTimestamp(new Date());
  return `${slug || 'prd'}-${datePart}-${timePart}`;
}

function prdDirectory(projectPath: string): string {
  return path.join(projectPath, PRD_DIRECTORY);
}

function prdFilePath(projectPath: string, id: string): string {
  return path.join(prdDirectory(projectPath), `${id}.yaml`);
}

function isNodeErrorWithCode(error: unknown, code: string): boolean {
  return (
    typeof error === 'object' &&
    error !== null &&
    'code' in error &&
    (error as { code?: string }).code === code
  );
}

async function writeFileAtomic(filePath: string, content: string): Promise<void> {
  const directory = path.dirname(filePath);
  await fs.mkdir(directory, { recursive: true });

  const tempPath = path.join(
    directory,
    `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random()
      .toString(16)
      .slice(2)}`,
  );

  await fs.writeFile(tempPath, content, 'utf8');
  await fs.rename(tempPath, filePath);
}

export async function createPrd(options: CreatePrdOptions): Promise<PrdDocument> {
  const resolvedProjectPath = resolveProjectPath(options.projectPath);
  const template = resolveTemplate(options.template);
  const now = new Date().toISOString();

  const document: PrdDocument = {
    id: buildPrdId(options.name),
    title: options.name.trim(),
    status: 'draft',
    projectPath: resolvedProjectPath,
    template: template.id,
    sections: template.sections.map((section) => ({
      id: section.id,
      title: section.title,
      fields: Object.fromEntries(section.fields.map((field) => [field, ''])),
    })),
    createdAt: now,
    updatedAt: now,
  };

  await savePrd(document);
  return document;
}

export async function loadPrd(projectPath: string): Promise<PrdDocument> {
  const documents = await listPrds(projectPath);
  if (documents.length === 0) {
    const resolvedProjectPath = resolveProjectPath(projectPath);
    throw new Error(`No PRD documents found in ${prdDirectory(resolvedProjectPath)}`);
  }

  return documents[0]!;
}

export async function savePrd(doc: PrdDocument): Promise<void> {
  const normalized = prdDocumentSchema.parse({
    ...doc,
    projectPath: resolveProjectPath(doc.projectPath),
  });

  const filePath = prdFilePath(normalized.projectPath, normalized.id);
  const serialized = yaml.dump(normalized, {
    noRefs: true,
    sortKeys: false,
    lineWidth: 120,
  });

  const content = serialized.endsWith('\n') ? serialized : `${serialized}\n`;
  await writeFileAtomic(filePath, content);
}

export async function listPrds(projectPath: string): Promise<PrdDocument[]> {
  const resolvedProjectPath = resolveProjectPath(projectPath);
  const directory = prdDirectory(resolvedProjectPath);

  let entries: Dirent[];
  try {
    entries = await fs.readdir(directory, { withFileTypes: true, encoding: 'utf8' });
  } catch (error) {
    if (isNodeErrorWithCode(error, 'ENOENT')) {
      return [];
    }

    throw error;
  }

  const documents: PrdDocument[] = [];

  for (const entry of entries) {
    if (!entry.isFile()) {
      continue;
    }

    const ext = path.extname(entry.name);
    if (!PRD_FILE_EXTENSIONS.has(ext)) {
      continue;
    }

    const filePath = path.join(directory, entry.name);
    const raw = await fs.readFile(filePath, 'utf8');

    let parsed: unknown;
    try {
      parsed = yaml.load(raw);
    } catch (error) {
      throw new Error(`Failed to parse PRD file ${filePath}: ${String(error)}`);
    }

    const document = prdDocumentSchema.parse(parsed);
    documents.push(document);
  }

  documents.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
  return documents;
}
