import { readFile, writeFile, readdir, stat, mkdir } from 'fs/promises';
import { join, dirname, basename, extname } from 'path';

const VAULT_PATH = process.env['OBSIDIAN_VAULT_PATH'] ?? '/vaults';
const SYNCTHING_URL = process.env['SYNCTHING_URL'] ?? 'http://host.docker.internal:8384';
const SYNCTHING_API_KEY = process.env['SYNCTHING_API_KEY'] ?? '';
const SYNCTHING_FOLDER_ID = process.env['SYNCTHING_FOLDER_ID'] ?? 'obsidian-vault';

function parseFrontmatter(content: string): { meta: Record<string, unknown>; body: string } {
  if (!content.startsWith('---\n') && !content.startsWith('---\r\n')) {
    return { meta: {}, body: content };
  }
  const end = content.indexOf('\n---', 4);
  if (end === -1) return { meta: {}, body: content };

  const yaml = content.slice(4, end);
  const body = content.slice(end + 4).replace(/^\r?\n/, '');
  const meta: Record<string, unknown> = {};

  for (const line of yaml.split('\n')) {
    const colon = line.indexOf(':');
    if (colon === -1) continue;
    const key = line.slice(0, colon).trim();
    const val = line.slice(colon + 1).trim();
    if (!key) continue;
    if (key === 'tags') {
      // Support "tags: [a, b]" or "tags: a, b"
      meta['tags'] = val
        .replace(/[\[\]]/g, '')
        .split(',')
        .map((t) => t.trim())
        .filter(Boolean);
    } else {
      meta[key] = val;
    }
  }
  return { meta, body };
}

function extractTitle(filePath: string, content: string): string {
  const { meta, body } = parseFrontmatter(content);
  if (meta['title']) return String(meta['title']);
  const h1 = body.match(/^#\s+(.+)/m);
  if (h1) return h1[1].trim();
  return basename(filePath, '.md');
}

async function getAllNotes(dir: string = VAULT_PATH): Promise<string[]> {
  let entries;
  try {
    entries = await readdir(dir, { withFileTypes: true });
  } catch {
    return [];
  }
  const files: string[] = [];
  for (const entry of entries) {
    if (entry.name.startsWith('.')) continue;
    const full = join(dir, entry.name);
    if (entry.isDirectory()) {
      files.push(...(await getAllNotes(full)));
    } else if (entry.isFile() && extname(entry.name) === '.md') {
      files.push(full);
    }
  }
  return files;
}

function relPath(fullPath: string): string {
  return fullPath.startsWith(VAULT_PATH)
    ? fullPath.slice(VAULT_PATH.length).replace(/^\//, '')
    : fullPath;
}

export interface NoteResult {
  path: string;
  title: string;
  excerpt: string;
  tags: string[];
  modified_date: string;
}

export interface FullNote {
  path: string;
  title: string;
  content: string;
  tags: string[];
  links: string[];
  modified_date: string;
}

export async function searchNotes(
  query: string,
  tags?: string[],
  limit = 10,
  pathFilter?: string
): Promise<NoteResult[]> {
  const files = await getAllNotes();
  const results: NoteResult[] = [];
  const q = query.toLowerCase();

  for (const file of files) {
    if (results.length >= limit) break;

    const rel = relPath(file);
    if (pathFilter && !rel.toLowerCase().includes(pathFilter.toLowerCase())) continue;

    let content: string;
    try {
      content = await readFile(file, 'utf-8');
    } catch {
      continue;
    }

    const { meta, body } = parseFrontmatter(content);
    const fileTags = (meta['tags'] as string[]) ?? [];
    const title = extractTitle(file, content);

    if (tags && tags.length > 0) {
      const hasAllTags = tags.every((t) => fileTags.includes(t));
      if (!hasAllTags) continue;
    }

    const matchesQuery =
      !q ||
      title.toLowerCase().includes(q) ||
      body.toLowerCase().includes(q) ||
      rel.toLowerCase().includes(q);

    if (!matchesQuery) continue;

    let excerpt = body.slice(0, 200).replace(/\n+/g, ' ').trim();
    if (q) {
      const idx = body.toLowerCase().indexOf(q);
      if (idx > 0) {
        excerpt = body.slice(Math.max(0, idx - 50), idx + 150).replace(/\n+/g, ' ').trim();
      }
    }
    if (excerpt.length >= 150) excerpt += '...';

    const s = await stat(file);
    results.push({
      path: rel,
      title,
      excerpt,
      tags: fileTags,
      modified_date: s.mtime.toISOString(),
    });
  }

  return results;
}

export async function getNote(notePath: string): Promise<FullNote> {
  let fullPath = notePath.startsWith('/') ? notePath : join(VAULT_PATH, notePath);

  // If path doesn't end in .md, try adding it
  if (!fullPath.endsWith('.md')) fullPath += '.md';

  try {
    await stat(fullPath);
  } catch {
    // Fall back to searching by title / filename match
    const files = await getAllNotes();
    const needle = notePath.replace(/\.md$/i, '').toLowerCase();
    const match = files.find(
      (f) =>
        basename(f, '.md').toLowerCase() === needle ||
        relPath(f).replace(/\.md$/i, '').toLowerCase() === needle
    );
    if (!match) throw new Error(`Note not found: ${notePath}`);
    fullPath = match;
  }

  const content = await readFile(fullPath, 'utf-8');
  const { meta, body } = parseFrontmatter(content);
  const title = extractTitle(fullPath, content);
  const s = await stat(fullPath);

  const links = [...body.matchAll(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g)].map((m) => m[1].trim());

  return {
    path: relPath(fullPath),
    title,
    content,
    tags: (meta['tags'] as string[]) ?? [],
    links: [...new Set(links)],
    modified_date: s.mtime.toISOString(),
  };
}

export async function appendToNote(
  notePath: string,
  content: string,
  createIfMissing = true,
  header?: string
): Promise<{ success: boolean; path: string; bytes_written: number }> {
  let fullPath = notePath.startsWith('/') ? notePath : join(VAULT_PATH, notePath);
  if (!fullPath.endsWith('.md')) fullPath += '.md';

  let existing = '';
  try {
    existing = await readFile(fullPath, 'utf-8');
  } catch {
    if (!createIfMissing) throw new Error(`Note not found: ${notePath}`);
    await mkdir(dirname(fullPath), { recursive: true });
  }

  const separator = existing && !existing.endsWith('\n') ? '\n' : '';
  const toAppend = header
    ? `${separator}\n## ${header}\n${content}\n`
    : `${separator}${content}\n`;

  await writeFile(fullPath, existing + toAppend, 'utf-8');

  return {
    success: true,
    path: relPath(fullPath),
    bytes_written: Buffer.byteLength(toAppend, 'utf-8'),
  };
}

export async function updateNote(
  notePath: string,
  content: string,
): Promise<{ success: boolean; path: string; bytes_written: number }> {
  let fullPath = notePath.startsWith('/') ? notePath : join(VAULT_PATH, notePath);
  if (!fullPath.endsWith('.md')) fullPath += '.md';
  await mkdir(dirname(fullPath), { recursive: true });
  await writeFile(fullPath, content, 'utf-8');
  return { success: true, path: relPath(fullPath), bytes_written: Buffer.byteLength(content, 'utf-8') };
}

export async function getSyncStatus(): Promise<{
  status: string;
  last_sync: string | null;
  vault_size: number;
  pending_changes: number;
}> {
  if (!SYNCTHING_API_KEY) {
    return { status: 'unconfigured – set SYNCTHING_API_KEY', last_sync: null, vault_size: 0, pending_changes: 0 };
  }

  try {
    const url = `${SYNCTHING_URL}/rest/db/status?folder=${SYNCTHING_FOLDER_ID}`;
    const res = await fetch(url, {
      headers: { 'X-API-Key': SYNCTHING_API_KEY },
      signal: AbortSignal.timeout(5000),
    });
    if (!res.ok) throw new Error(`Syncthing API returned ${res.status}`);

    const data = (await res.json()) as {
      state: string;
      stateChanged: string;
      localBytes: number;
      needFiles: number;
    };

    return {
      status: data.state,
      last_sync: data.stateChanged ?? null,
      vault_size: data.localBytes ?? 0,
      pending_changes: data.needFiles ?? 0,
    };
  } catch (err) {
    return {
      status: `error: ${(err as Error).message}`,
      last_sync: null,
      vault_size: 0,
      pending_changes: 0,
    };
  }
}
