import type { Doc, Id } from "@code/backend/convex/_generated/dataModel";

// ---------------------------------------------------------------------------
// Work Unit card projection — pure functions that turn Convex documents into
// the collapsed/expanded Work Unit views used by the Work OS surface.
//
// Counts are derived from issue-scoped evidence: events for artifacts/steps/
// PRs, and authenticated signalIssueAttachments for linked Signals. We never
// use project-wide artifact lists or signal lists.
// ---------------------------------------------------------------------------

export type ProjectEvent = Doc<"projectEvents">;
export type ProjectIssue = Doc<"projectIssues">;

/** A Signal linked to a Work Unit via signalIssueAttachments. */
export interface LinkedSignal {
  readonly signalId: Id<"signals">;
  readonly title: string;
  readonly summary: string;
  readonly sourceCount: number;
  readonly createdAt: number;
}

export interface WorkUnitCard {
  readonly issueId: Id<"projectIssues">;
  readonly number: number;
  readonly title: string;
  readonly status: ProjectIssue["status"];
  readonly summary: string;
  readonly signalCount: number;
  readonly currentActivity: string | null;
  readonly stepCount: number;
  readonly artifactCount: number;
  readonly hasPullRequest: boolean;
  readonly needsInput: boolean;
  readonly updatedAt: number;
}

export interface WorkUnitActivityItem {
  readonly label: string;
  readonly detail: string;
  readonly time: string;
  readonly kind: string;
  readonly createdAt: number;
}

export interface WorkUnitDetail {
  readonly issue: ProjectIssue;
  readonly activity: readonly WorkUnitActivityItem[];
  readonly stepCount: number;
  readonly artifactCount: number;
  readonly artifactPaths: readonly string[];
  readonly signalCount: number;
  readonly linkedSignals: readonly LinkedSignal[];
  readonly hasPullRequest: boolean;
  readonly pullRequestUrl: string | null;
  readonly pullRequestNumber: number | null;
  readonly needsInput: boolean;
  readonly latestSummary: string | null;
}

// ---------------------------------------------------------------------------
// Time formatting
// ---------------------------------------------------------------------------

export const formatRelativeTime = (timestamp: number): string => {
  const minutes = Math.max(0, Math.round((Date.now() - timestamp) / 60_000));
  if (minutes < 1) {
    return "just now";
  }
  if (minutes < 60) {
    return `${minutes}m ago`;
  }
  const hours = Math.round(minutes / 60);
  if (hours < 24) {
    return `${hours}h ago`;
  }
  return `${Math.round(hours / 24)}d ago`;
};

export const formatClockTime = (timestamp: number): string =>
  new Intl.DateTimeFormat(undefined, {
    hour: "numeric",
    minute: "2-digit",
  }).format(timestamp);

// ---------------------------------------------------------------------------
// Event filtering
// ---------------------------------------------------------------------------

export const eventsForIssue = (
  events: readonly ProjectEvent[] | undefined,
  issueId: Id<"projectIssues">
): readonly ProjectEvent[] =>
  (events ?? []).filter((e) => e.issueId === issueId);

// ---------------------------------------------------------------------------
// Event-to-activity projection
// ---------------------------------------------------------------------------

interface ActivityProjection {
  readonly label: string;
  readonly detail: string;
}

const getString = (
  data: Record<string, unknown>,
  key: string,
  fallback: string
): string => (typeof data[key] === "string" ? (data[key] as string) : fallback);

const ACTIVITY_HANDLERS: Record<
  string,
  (data: Record<string, unknown>, event: ProjectEvent) => ActivityProjection
> = {
  "agent.completed": (data) => ({
    detail: getString(data, "summary", "Run completed successfully"),
    label: "Work completed",
  }),
  "agent.failed": (data) => ({
    detail: getString(data, "error", "The run stopped unexpectedly"),
    label: "Run failed",
  }),
  "agent.needs-input": () => ({
    detail: "Waiting for a decision or missing information",
    label: "Needs your input",
  }),
  "agent.working": () => ({
    detail: "The project manager is progressing the issue",
    label: "Agent working",
  }),
  "agent.workspace.created": (data) => ({
    detail: `Workspace ready on ${getString(data, "branchName", "work branch")}`,
    label: "Workspace prepared",
  }),
  "artifact.updated": (data) => ({
    detail: `Evidence revised in ${getString(data, "path", "artifact")} (r${data.revision ?? "?"})`,
    label: "Evidence updated",
  }),
  "gitea.lifecycle.updated": (data) => ({
    detail: `${getString(data, "status", "updated")} on ${getString(data, "branch", "branch")}`,
    label: "Git lifecycle",
  }),
  "gitea.pull_request.created": (data) => {
    const pr = data.pullRequest as { number?: number } | undefined;
    return {
      detail: `Pull request #${pr?.number ?? "?"} opened for review`,
      label: "Pull request opened",
    };
  },
  "issue.created": (data) => ({
    detail: `Issue #${data.number ?? "?"} entered the loop`,
    label: "Work created",
  }),
  "issue.queued": (data) => ({
    detail: `Queued #${data.number ?? "?"} for the project manager`,
    label: "Work queued",
  }),
  "signal.attached": (data) => ({
    detail: `Signal "${getString(data, "signalProblemTitle", "Unknown")}" linked to this Work Unit`,
    label: "Signal linked",
  }),
};

const projectEventToActivity = (event: ProjectEvent): ActivityProjection => {
  const data = (event.data ?? {}) as Record<string, unknown>;
  const handler = ACTIVITY_HANDLERS[event.kind];
  if (handler) {
    return handler(data, event);
  }
  return { detail: event.kind, label: "Activity" };
};

export const buildActivityTimeline = (
  issueEvents: readonly ProjectEvent[]
): readonly WorkUnitActivityItem[] =>
  [...issueEvents]
    .toSorted((a, b) => b.createdAt - a.createdAt)
    .map((event) => {
      const projection = projectEventToActivity(event);
      return {
        createdAt: event.createdAt,
        detail: projection.detail,
        kind: event.kind,
        label: projection.label,
        time: formatRelativeTime(event.createdAt),
      };
    });

// ---------------------------------------------------------------------------
// PR extraction from issue-scoped events
// ---------------------------------------------------------------------------

export interface PullRequestInfo {
  readonly hasPR: boolean;
  readonly url: string | null;
  readonly number: number | null;
}

export const extractPullRequestFromEvents = (
  issueEvents: readonly ProjectEvent[]
): PullRequestInfo => {
  const prEvent = issueEvents.find(
    (e) => e.kind === "gitea.pull_request.created"
  );
  if (!prEvent) {
    return { hasPR: false, number: null, url: null };
  }
  const data = prEvent.data as {
    pullRequest?: { url?: string; number?: number };
  };
  const pr = data.pullRequest;
  return {
    hasPR: true,
    number: pr?.number ?? null,
    url: pr?.url ?? null,
  };
};

// ---------------------------------------------------------------------------
// Latest agent summary
// ---------------------------------------------------------------------------

export const extractLatestSummary = (
  issueEvents: readonly ProjectEvent[]
): string | null => {
  const summaryEvent = [...issueEvents]
    .toSorted((a, b) => b.createdAt - a.createdAt)
    .find((e) => e.kind === "agent.completed" || e.kind === "agent.failed");
  if (!summaryEvent) {
    return null;
  }
  const data = summaryEvent.data as { summary?: string; error?: string };
  if (summaryEvent.kind === "agent.completed") {
    return data.summary ?? null;
  }
  return data.error ?? null;
};

// ---------------------------------------------------------------------------
// Artifact path extraction from issue-scoped artifact.updated events
// ---------------------------------------------------------------------------

export const extractArtifactPaths = (
  issueEvents: readonly ProjectEvent[]
): readonly string[] => {
  const paths = new Set<string>();
  for (const event of issueEvents) {
    if (event.kind !== "artifact.updated") {
      continue;
    }
    const data = event.data as { path?: string };
    if (typeof data.path === "string") {
      paths.add(data.path);
    }
  }
  return [...paths].toSorted();
};

// ---------------------------------------------------------------------------
// Card builder — derives counts from issue-scoped events + linked signals
// ---------------------------------------------------------------------------

export const buildWorkUnitCard = ({
  issue,
  issueEvents,
  linkedSignals,
}: {
  readonly issue: ProjectIssue;
  readonly issueEvents: readonly ProjectEvent[];
  readonly linkedSignals: readonly LinkedSignal[];
}): WorkUnitCard => {
  const sorted = [...issueEvents].toSorted((a, b) => b.createdAt - a.createdAt);
  const [latestEvent] = sorted;
  const currentActivity = latestEvent
    ? projectEventToActivity(latestEvent).label
    : null;
  const prInfo = extractPullRequestFromEvents(issueEvents);
  const summary = extractLatestSummary(issueEvents) ?? issue.body.slice(0, 140);
  const artifactPaths = extractArtifactPaths(issueEvents);

  return {
    artifactCount: artifactPaths.length,
    currentActivity,
    hasPullRequest: prInfo.hasPR,
    issueId: issue._id,
    needsInput: issue.status === "needs-input",
    number: issue.number,
    signalCount: linkedSignals.length,
    status: issue.status,
    stepCount: issueEvents.length,
    summary,
    title: issue.title,
    updatedAt: issue.updatedAt,
  };
};

// ---------------------------------------------------------------------------
// Detail builder — same per-issue derivation, plus linked signals
// ---------------------------------------------------------------------------

export const buildWorkUnitDetail = ({
  issue,
  issueEvents,
  linkedSignals,
}: {
  readonly issue: ProjectIssue;
  readonly issueEvents: readonly ProjectEvent[];
  readonly linkedSignals: readonly LinkedSignal[];
}): WorkUnitDetail => {
  const activity = buildActivityTimeline(issueEvents);
  const prInfo = extractPullRequestFromEvents(issueEvents);
  const artifactPaths = extractArtifactPaths(issueEvents);

  return {
    activity,
    artifactCount: artifactPaths.length,
    artifactPaths,
    hasPullRequest: prInfo.hasPR,
    issue,
    latestSummary: extractLatestSummary(issueEvents),
    linkedSignals,
    needsInput: issue.status === "needs-input",
    pullRequestNumber: prInfo.number,
    pullRequestUrl: prInfo.url,
    signalCount: linkedSignals.length,
    stepCount: issueEvents.length,
  };
};
