import { randomUUID } from "node:crypto";
import { desc, eq } from "drizzle-orm";
import {
  type DashboardDocument,
  type DashboardValidationFailure,
} from "@dimensionlab/dashboard-model";
import {
  type DashboardDatabaseConnection,
  openDashboardDatabase,
} from "./connection";
import {
  dashboardDocuments,
  dashboardRevisions,
  type DashboardRevisionOperation,
} from "./schema";
import { migrateDashboardDocumentForPersistence } from "./model-migrations";

const DEFAULT_DASHBOARD_ID = "primary";
const DEFAULT_ACTOR = "system";

export interface DashboardRevision {
  id: string;
  dashboardId: string;
  schemaVersion: string;
  document: DashboardDocument;
  actor: string;
  message: string | null;
  operation: DashboardRevisionOperation;
  sourceRevisionId: string | null;
  createdAt: Date;
}

export interface ActiveDashboard {
  dashboardId: string;
  currentRevisionId: string;
  document: DashboardDocument;
  revision: DashboardRevision;
  createdAt: Date;
  updatedAt: Date;
}

export interface DashboardWriteMetadata {
  actor?: string;
  message?: string;
}

export interface DashboardStoreOptions {
  databaseUrl?: string;
  dashboardId?: string;
}

export interface DashboardStore {
  getActiveDashboard(): ActiveDashboard | null;
  getRevision(revisionId: string): DashboardRevision | null;
  listRevisions(limit?: number): DashboardRevision[];
  seedDashboardIfEmpty(
    document: DashboardDocument,
    metadata?: DashboardWriteMetadata,
  ): DashboardRevision;
  commitDashboard(
    document: DashboardDocument,
    metadata?: DashboardWriteMetadata,
  ): DashboardRevision;
  rollbackToRevision(
    revisionId: string,
    metadata?: DashboardWriteMetadata,
  ): DashboardRevision;
  close(): void;
}

export class DashboardPersistenceValidationError extends Error {
  readonly failure: DashboardValidationFailure;

  constructor(failure: DashboardValidationFailure) {
    super(`Invalid dashboard document: ${failure.errors.join("; ")}`);
    this.name = "DashboardPersistenceValidationError";
    this.failure = failure;
  }
}

export class DashboardRevisionNotFoundError extends Error {
  constructor(revisionId: string) {
    super(`Dashboard revision not found: ${revisionId}`);
    this.name = "DashboardRevisionNotFoundError";
  }
}

export function createDashboardStore(
  options: DashboardStoreOptions = {},
): DashboardStore {
  const connection = openDashboardDatabase(options.databaseUrl);
  return new SqliteDashboardStore(connection, options.dashboardId || DEFAULT_DASHBOARD_ID);
}

class SqliteDashboardStore implements DashboardStore {
  constructor(
    private readonly connection: DashboardDatabaseConnection,
    private readonly dashboardId: string,
  ) {}

  getActiveDashboard(): ActiveDashboard | null {
    const dashboard = this.connection.db
      .select()
      .from(dashboardDocuments)
      .where(eq(dashboardDocuments.id, this.dashboardId))
      .get();

    if (!dashboard?.currentRevisionId) return null;

    const revision = this.getRevision(dashboard.currentRevisionId);
    if (!revision) return null;

    return {
      dashboardId: dashboard.id,
      currentRevisionId: dashboard.currentRevisionId,
      document: revision.document,
      revision,
      createdAt: dashboard.createdAt,
      updatedAt: dashboard.updatedAt,
    };
  }

  getRevision(revisionId: string): DashboardRevision | null {
    const row = this.connection.db
      .select()
      .from(dashboardRevisions)
      .where(eq(dashboardRevisions.id, revisionId))
      .get();

    return row ? toRevision(row) : null;
  }

  listRevisions(limit = 50): DashboardRevision[] {
    return this.connection.db
      .select()
      .from(dashboardRevisions)
      .where(eq(dashboardRevisions.dashboardId, this.dashboardId))
      .orderBy(desc(dashboardRevisions.createdAt))
      .limit(limit)
      .all()
      .map(toRevision);
  }

  seedDashboardIfEmpty(
    document: DashboardDocument,
    metadata: DashboardWriteMetadata = {},
  ): DashboardRevision {
    const active = this.getActiveDashboard();
    if (active) return active.revision;

    return this.writeRevision(document, "seed", metadata);
  }

  commitDashboard(
    document: DashboardDocument,
    metadata: DashboardWriteMetadata = {},
  ): DashboardRevision {
    return this.writeRevision(document, "commit", metadata);
  }

  rollbackToRevision(
    revisionId: string,
    metadata: DashboardWriteMetadata = {},
  ): DashboardRevision {
    const revision = this.getRevision(revisionId);
    if (!revision || revision.dashboardId !== this.dashboardId) {
      throw new DashboardRevisionNotFoundError(revisionId);
    }

    return this.writeRevision(revision.document, "rollback", metadata, revision.id);
  }

  close() {
    this.connection.close();
  }

  private writeRevision(
    document: DashboardDocument,
    operation: DashboardRevisionOperation,
    metadata: DashboardWriteMetadata,
    sourceRevisionId: string | null = null,
  ): DashboardRevision {
    const migration = migrateDashboardDocumentForPersistence(document);
    if (!migration.valid) {
      throw new DashboardPersistenceValidationError(migration.failure);
    }

    const now = this.nextRevisionTimestamp();
    const revision: DashboardRevision = {
      id: randomUUID(),
      dashboardId: this.dashboardId,
      schemaVersion: migration.document.schemaVersion,
      document: structuredClone(migration.document),
      actor: metadata.actor || DEFAULT_ACTOR,
      message: metadata.message || null,
      operation,
      sourceRevisionId,
      createdAt: now,
    };

    this.connection.db.transaction((tx) => {
      tx.insert(dashboardRevisions).values({
        id: revision.id,
        dashboardId: revision.dashboardId,
        schemaVersion: revision.schemaVersion,
        document: revision.document,
        actor: revision.actor,
        message: revision.message,
        operation: revision.operation,
        sourceRevisionId: revision.sourceRevisionId,
        createdAt: revision.createdAt,
      }).run();

      tx.insert(dashboardDocuments)
        .values({
          id: this.dashboardId,
          currentRevisionId: revision.id,
          createdAt: now,
          updatedAt: now,
        })
        .onConflictDoUpdate({
          target: dashboardDocuments.id,
          set: {
            currentRevisionId: revision.id,
            updatedAt: now,
          },
        })
        .run();
    });

    return revision;
  }

  private nextRevisionTimestamp(): Date {
    const active = this.getActiveDashboard();
    const activeUpdatedAtMs = active?.updatedAt.getTime() || 0;

    return new Date(Math.max(Date.now(), activeUpdatedAtMs + 1));
  }
}

type DashboardRevisionRow = typeof dashboardRevisions.$inferSelect;

function toRevision(row: DashboardRevisionRow): DashboardRevision {
  const migration = migrateDashboardDocumentForPersistence(row.document);
  if (!migration.valid) {
    throw new DashboardPersistenceValidationError(migration.failure);
  }

  return {
    id: row.id,
    dashboardId: row.dashboardId,
    schemaVersion: row.schemaVersion,
    document: migration.document,
    actor: row.actor,
    message: row.message,
    operation: row.operation as DashboardRevisionOperation,
    sourceRevisionId: row.sourceRevisionId,
    createdAt: row.createdAt,
  };
}
