import type {
  Entity,
  EntityKind,
  GraphProvider,
  Property,
  Relation,
} from "@enesis/editor"
import type Database from "@tauri-apps/plugin-sql"

/**
 * Convenience factory: create a GraphService from an open SQLite database.
 *
 * The graph is a read-only projection over the SQLite index. It never
 * writes to the database or filesystem — every entity can be regenerated
 * from the markdown files on disk.
 *
 * Use this in application code (composables, page components) that have
 * access to a Database connection via the workspace store.
 *
 * @example
 * const graph = createGraphService(db)
 * const pages = await graph.getEntities("page")
 * const backlinks = await graph.getBacklinks("My Page")
 */
export function createGraphService(db: Database): GraphService {
  return new GraphService(new SQLiteGraphProvider(db))
}

/**
 * SQLite-backed implementation of GraphProvider.
 *
 * Transforms raw SQLite rows into Entity/Relation/Property objects.
 * This is a projection layer — it never writes to the database.
 * SQLite is itself a projection of the markdown files on disk.
 *
 * IDs live in a flat namespace (page paths, block nanoIds, tag names).
 * See EntityKind JSDoc in packages/editor/src/services/graph.ts for
 * collision considerations and future plans for composite keys.
 */
export class SQLiteGraphProvider implements GraphProvider {
  constructor(private db: Database) {}

  /**
   * Return all entities of the given kind, or all entities if kind is
   * omitted. Pages are ordered by title, blocks by most-recently-modified
   * (up to 1000), tags alphabetically.
   *
   * When `kind` is undefined, all three kinds are batched into a single
   * returned array rather than early-returning on the first match.
   */
  async getEntities(kind?: EntityKind): Promise<Entity[]> {
    const results: Entity[] = []

    if (!kind || kind === "page") {
      const pages = await this.db.select<{ path: string; title: string }[]>(
        "SELECT path, title FROM pages ORDER BY title",
      )
      for (const p of pages) {
        results.push({
          id: p.path,
          kind: "page",
          title:
            p.title || p.path.split("/").pop()?.replace(/\.md$/, "") || p.path,
          path: p.path,
        })
      }
    }

    if (!kind || kind === "block") {
      const blocks = await this.db.select<
        { id: string; content: string; page_path: string }[]
      >(
        "SELECT id, content, page_path FROM blocks ORDER BY modified_at DESC LIMIT 1000",
      )
      for (const b of blocks) {
        results.push({
          id: b.id,
          kind: "block",
          title: b.content.slice(0, 80),
          path: b.page_path,
        })
      }
    }

    if (!kind || kind === "tag") {
      const tags = await this.db.select<{ target: string }[]>(
        "SELECT DISTINCT target FROM links WHERE link_type = 'tag' ORDER BY target",
      )
      for (const t of tags) {
        results.push({ id: t.target, kind: "tag", title: t.target })
      }
    }

    return results
  }

  /**
   * Batch entity lookup. Performs three queries (pages, blocks, tags)
   * using SQL `IN (...)` and merges results. Returns only entities that
   * exist — missing IDs are silently dropped.
   *
   * This is the building block for avoiding N+1 query patterns in
   * GraphService methods like getIncoming and queryByProperty.
   */
  async getEntitiesByIds(ids: string[]): Promise<Entity[]> {
    if (ids.length === 0) return []

    const placeholders = ids.map((_, i) => `$${i + 1}`).join(", ")

    const pages = await this.db.select<{ path: string; title: string }[]>(
      `SELECT path, title FROM pages WHERE path IN (${placeholders})`,
      ids,
    )
    const blocks = await this.db.select<
      { id: string; content: string; page_path: string }[]
    >(
      `SELECT id, content, page_path FROM blocks WHERE id IN (${placeholders})`,
      ids,
    )
    const tags = await this.db.select<{ target: string }[]>(
      `SELECT target FROM links WHERE link_type = 'tag' AND target IN (${placeholders}) GROUP BY target`,
      ids,
    )

    const results: Entity[] = []

    for (const p of pages) {
      results.push({
        id: p.path,
        kind: "page",
        title:
          p.title || p.path.split("/").pop()?.replace(/\.md$/, "") || p.path,
        path: p.path,
      })
    }

    for (const b of blocks) {
      results.push({
        id: b.id,
        kind: "block",
        title: b.content.slice(0, 80),
        path: b.page_path,
      })
    }

    for (const t of tags) {
      results.push({ id: t.target, kind: "tag", title: t.target })
    }

    return results
  }

  /**
   * Look up a single entity by ID. Checks pages, then blocks, then tags.
   * Returns undefined if the ID doesn't match any known entity.
   *
   * Delegates to getEntitiesByIds for consistency — a single-ID batch
   * query has negligible overhead vs a dedicated query.
   */
  async getEntity(id: string): Promise<Entity | undefined> {
    const entities = await this.getEntitiesByIds([id])
    return entities[0]
  }

  /**
   * Query the relation graph. All parameters are optional:
   *
   * - `from`: filter by source_block_id (the entity that points outward)
   * - `to`: filter by target (the entity being pointed to)
   * - `type`: restrict to a specific relation type
   *
   * `type="contains"` is handled as a special case — it queries the
   * implicit page→block containment via `blocks.page_path` rather than
   * the `links` table.
   *
   * Relation directions:
   * - references: from=block_id, to=page_name_or_block_id
   * - tagged: from=block_id, to=tag_name
   * - contains: from=page_path, to=block_id (page CONTAINS block)
   */
  async getRelations(
    from?: string,
    to?: string,
    type?: Relation["type"],
  ): Promise<Relation[]> {
    const conditions: string[] = []
    const params: unknown[] = []

    if (from) {
      conditions.push(`source_block_id = $${params.length + 1}`)
      params.push(from)
    }
    if (to) {
      conditions.push(`target = $${params.length + 1}`)
      params.push(to)
    }
    if (type === "references") {
      conditions.push("link_type IN ('page-ref', 'block-ref')")
    } else if (type === "tagged") {
      conditions.push("link_type = 'tag'")
    } else if (type === "contains") {
      // handled separately below — containment is implicit from blocks.page_path
    }

    const results: Relation[] = []

    if (type !== "contains") {
      const where =
        conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""
      const rows = await this.db.select<
        { source_block_id: string; target: string; link_type: string }[]
      >(
        `SELECT source_block_id, target, link_type FROM links ${where} ORDER BY id`,
        params,
      )

      for (const r of rows) {
        results.push({
          from: r.source_block_id,
          to: r.target,
          type: r.link_type === "tag" ? "tagged" : "references",
        })
      }
    }

    // contains: page CONTAINS block
    // (from: page_path, to: block_id)
    if ((!type || type === "contains") && (to || !from)) {
      const containsParams: unknown[] = to ? [to] : []
      const rows = await this.db.select<
        { block_id: string; page_path: string }[]
      >(
        "SELECT id AS block_id, page_path FROM blocks" +
          (to ? " WHERE id = $1" : ""),
        containsParams,
      )
      for (const r of rows) {
        results.push({
          from: r.page_path,
          to: r.block_id,
          type: "contains",
        })
      }
    }

    return results
  }

  /**
   * Query the properties table. All parameters optional:
   *
   * - `entityId`: filter to a single block's properties
   * - `key`: filter to a specific property key (e.g. "priority")
   *
   * When both are omitted, returns all properties across all blocks
   * (useful for building autocomplete suggestions).
   */
  async getProperties(entityId?: string, key?: string): Promise<Property[]> {
    const conditions: string[] = []
    const params: unknown[] = []

    if (entityId) {
      conditions.push(`block_id = $${params.length + 1}`)
      params.push(entityId)
    }
    if (key) {
      conditions.push(`key = $${params.length + 1}`)
      params.push(key)
    }

    const where =
      conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""
    const rows = await this.db.select<
      { block_id: string; key: string; value: string }[]
    >(
      `SELECT block_id, key, value FROM properties ${where} ORDER BY key`,
      params,
    )

    return rows.map((r) => ({
      entityId: r.block_id,
      key: r.key,
      value: r.value,
    }))
  }
}

/**
 * High-level graph query service.
 *
 * Provides convenience methods for common graph traversal patterns that
 * would otherwise require writing SQL JOINs everywhere. Wraps a
 * GraphProvider (typically SQLiteGraphProvider) and adds traversal
 * logic on top of the raw entity/relation data.
 *
 * Performance note: all batch methods (getIncoming, getOutgoing,
 * queryByProperty) collect IDs and perform a single
 * getEntitiesByIds call rather than N individual getEntity calls.
 * This avoids the N+1 query problem.
 */
export class GraphService {
  constructor(private provider: GraphProvider) {}

  /**
   * List all entities of the given kind, or all entities if kind is omitted.
   * Pass-through to the provider.
   *
   * Pages are ordered by title, blocks by recency (up to 1000), tags alphabetically.
   */
  async getEntities(kind?: EntityKind): Promise<Entity[]> {
    return this.provider.getEntities(kind)
  }

  /**
   * Look up a single entity by ID.
   * Returns undefined if the ID doesn't match any known entity.
   */
  async getEntity(id: string): Promise<Entity | undefined> {
    return this.provider.getEntity(id)
  }

  /**
   * Batch entity lookup. See SQLiteGraphProvider.getEntitiesByIds.
   */
  async getEntitiesByIds(ids: string[]): Promise<Entity[]> {
    return this.provider.getEntitiesByIds(ids)
  }

  /**
   * Get all entities that point TO the given entityId (incoming edges).
   *
   * For example, getIncoming("My Page") returns all blocks that
   * contain `[[My Page]]` references, along with the relation info.
   *
   * Uses batch entity lookup internally — exactly 2 queries regardless
   * of how many incoming relations exist.
   */
  async getIncoming(
    entityId: string,
  ): Promise<{ from: Entity; via: Relation }[]> {
    const relations = await this.provider.getRelations(undefined, entityId)
    if (relations.length === 0) return []

    const fromIds = [...new Set(relations.map((r) => r.from))]
    const entities = await this.provider.getEntitiesByIds(fromIds)
    const entityMap = new Map(entities.map((e) => [e.id, e]))

    return relations
      .map((rel) => {
        const entity = entityMap.get(rel.from)
        return entity ? { from: entity, via: rel } : null
      })
      .filter((r): r is { from: Entity; via: Relation } => r !== null)
  }

  /**
   * Get all entities that the given entityId points TO (outgoing edges).
   *
   * For example, getOutgoing(blockId) returns the pages, blocks, or
   * tags that the block references via `[[page]]`, `((block))`, or `#tag`.
   *
   * Uses batch entity lookup internally — exactly 2 queries regardless
   * of how many outgoing relations exist.
   */
  async getOutgoing(
    entityId: string,
  ): Promise<{ to: Entity; via: Relation }[]> {
    const relations = await this.provider.getRelations(entityId)
    if (relations.length === 0) return []

    const toIds = [...new Set(relations.map((r) => r.to))]
    const entities = await this.provider.getEntitiesByIds(toIds)
    const entityMap = new Map(entities.map((e) => [e.id, e]))

    return relations
      .map((rel) => {
        const entity = entityMap.get(rel.to)
        return entity ? { to: entity, via: rel } : null
      })
      .filter((r): r is { to: Entity; via: Relation } => r !== null)
  }

  /**
   * Convenience alias for getIncoming. Returns all entities that
   * reference the given entityId. Useful for displaying backlinks
   * in the right sidebar.
   */
  async getBacklinks(
    entityId: string,
  ): Promise<{ from: Entity; via: Relation }[]> {
    return this.getIncoming(entityId)
  }

  /**
   * Get all properties for a given entity (block).
   * Returns an empty array if the entity has no properties or
   * doesn't exist.
   */
  async getProperties(entityId: string): Promise<Property[]> {
    return this.provider.getProperties(entityId)
  }

  /**
   * Find entities that have a property matching the given key and
   * optionally value.
   *
   * Examples:
   *   queryByProperty("priority", "high")   → blocks with priority:: high
   *   queryByProperty("status")             → all blocks with any status:: value
   *
   * Uses batch entity lookup — exactly 2 queries regardless of how
   * many matching properties exist.
   */
  async queryByProperty(key: string, value?: string): Promise<Entity[]> {
    const properties = await this.provider.getProperties(undefined, key)
    const filtered = value
      ? properties.filter((p) => p.value === value)
      : properties

    if (filtered.length === 0) return []

    const entityIds = [...new Set(filtered.map((p) => p.entityId))]
    return this.provider.getEntitiesByIds(entityIds)
  }

  /**
   * Query relations with optional filters.
   *
   * This is a direct pass-through to the provider for cases where
   * the higher-level getIncoming/getOutgoing don't provide the
   * filtering needed.
   */
  async findRelations(filter?: {
    from?: string
    to?: string
    type?: Relation["type"]
  }): Promise<Relation[]> {
    return this.provider.getRelations(filter?.from, filter?.to, filter?.type)
  }
}
