import { Array as EffectArray, Effect, Order, Schema } from "effect";

export { WorkStatus } from "./work-lifecycle";
export type { WorkStatus as WorkLifecycleStatus } from "./work-lifecycle";

const MeaningfulString = Schema.String.check(
  Schema.makeFilter((value) => value.trim().length > 0, {
    expected: "a non-empty string",
  })
);

export const WorkDraft = Schema.Struct({
  objective: MeaningfulString,
  title: MeaningfulString,
});
export type WorkDraft = typeof WorkDraft.Type;

export const SignalProblem = Schema.Struct({
  desiredOutcome: MeaningfulString,
  summary: MeaningfulString,
  title: MeaningfulString,
});

export const WorkCommandReason = Schema.Literals([
  "InvalidInput",
  "ProjectMismatch",
  "TitleTooLong",
  "ObjectiveTooLong",
]);
export type WorkCommandReason = typeof WorkCommandReason.Type;

export class WorkCommandError extends Schema.TaggedErrorClass<WorkCommandError>()(
  "WorkCommandError",
  {
    message: Schema.String,
    reason: WorkCommandReason,
  }
) {}

export const workDraftFromSignal = Effect.fn("Work.draftFromSignal")(
  function* workDraftFromSignal(input: unknown) {
    const problem = yield* Schema.decodeUnknownEffect(SignalProblem)(
      input
    ).pipe(
      Effect.mapError(
        () =>
          new WorkCommandError({
            message: "Signal is missing a valid problem statement",
            reason: "InvalidInput",
          })
      )
    );
    const title = problem.title.trim();
    const objective = problem.desiredOutcome.trim();

    if (title.length > 160) {
      return yield* new WorkCommandError({
        message: "Work title must be 160 characters or fewer",
        reason: "TitleTooLong",
      });
    }
    if (objective.length > 2000) {
      return yield* new WorkCommandError({
        message: "Work objective must be 2000 characters or fewer",
        reason: "ObjectiveTooLong",
      });
    }

    return { objective, title } satisfies WorkDraft;
  }
);

const WorkIdentity = Schema.Struct({
  organizationId: MeaningfulString,
  projectId: MeaningfulString,
  workId: MeaningfulString,
});

const SignalIdentity = Schema.Struct({
  organizationId: MeaningfulString,
  projectId: MeaningfulString,
  signalId: MeaningfulString,
  title: MeaningfulString,
});

const WorkAttachmentInput = Schema.Struct({
  signal: SignalIdentity,
  work: WorkIdentity,
});

export const WorkEventKind = Schema.Literals([
  "work.proposed",
  "signal.attached",
  "definition.requested",
  "definition.saved",
  "definition.revised",
  "definition.approved",
  "definition.invalidated",
  "question.created",
  "question.answered",
  "question.withdrawn",
  "design.requested",
  "design.saved",
  "design.revised",
  "design.approved",
  "design.invalidated",
  "planner.failed",
  "slice.started",
  "slice.completed",
  "slice.ready",
  "run.started",
  "run.completed",
  "run.cancelled",
  "attempt.claimed",
  "attempt.event",
  "attempt.completed",
  "attempt.reconciled",
  "resolver.decided",
  "artifact.recorded",
  "delivery.recorded",
  "delivery.updated",
]);
export type WorkEventKind = typeof WorkEventKind.Type;

export const WorkEventDraft = Schema.Struct({
  data: Schema.Struct({
    signalId: MeaningfulString,
    title: MeaningfulString,
  }),
  idempotencyKey: MeaningfulString,
  kind: WorkEventKind,
});
export type WorkEventDraft = typeof WorkEventDraft.Type;

export const workProposedEventFromSignal = Effect.fn(
  "Work.proposedEventFromSignal"
)(function* workProposedEventFromSignal(input: unknown) {
  const signal = yield* Schema.decodeUnknownEffect(SignalIdentity)(input).pipe(
    Effect.mapError(
      () =>
        new WorkCommandError({
          message: "Signal identity is invalid",
          reason: "InvalidInput",
        })
    )
  );
  return {
    data: { signalId: signal.signalId, title: signal.title.trim() },
    idempotencyKey: `proposed:${signal.signalId}`,
    kind: "work.proposed",
  } satisfies WorkEventDraft;
});

export const signalAttachedEvent = Effect.fn("Work.signalAttachedEvent")(
  function* signalAttachedEvent(input: unknown) {
    const attachment = yield* Schema.decodeUnknownEffect(WorkAttachmentInput)(
      input
    ).pipe(
      Effect.mapError(
        () =>
          new WorkCommandError({
            message: "Work attachment input is invalid",
            reason: "InvalidInput",
          })
      )
    );

    if (
      attachment.signal.organizationId !== attachment.work.organizationId ||
      attachment.signal.projectId !== attachment.work.projectId
    ) {
      return yield* new WorkCommandError({
        message: "Signal and Work must belong to the same project",
        reason: "ProjectMismatch",
      });
    }

    return {
      data: {
        signalId: attachment.signal.signalId,
        title: attachment.signal.title.trim(),
      },
      idempotencyKey: `signal:${attachment.signal.signalId}`,
      kind: "signal.attached",
    } satisfies WorkEventDraft;
  }
);

const WorkNoticeInput = Schema.Array(
  Schema.Struct({
    _id: MeaningfulString,
    events: Schema.Array(
      Schema.Struct({
        _id: MeaningfulString,
        createdAt: Schema.Number,
        kind: WorkEventKind,
      })
    ),
    signals: Schema.Array(
      Schema.Struct({
        sources: Schema.Array(
          Schema.Struct({ messageId: MeaningfulString, rawText: Schema.String })
        ),
      })
    ),
  })
);

export interface WorkNotice {
  readonly createdAt: number;
  readonly eventId: string;
  readonly sourceMessageIds: readonly string[];
  readonly sourceTexts: readonly string[];
  readonly workId: string;
}

const projectWorkNoticesEffect = Effect.fn("Work.projectNotices")(
  function* projectWorkNoticesEffect(input: unknown) {
    const works = yield* Schema.decodeUnknownEffect(WorkNoticeInput)(
      input
    ).pipe(
      Effect.mapError(
        () =>
          new WorkCommandError({
            message: "Work notice data is invalid",
            reason: "InvalidInput",
          })
      )
    );

    const notices = works.flatMap((work) => {
      const sourceMessageIds = work.signals.flatMap((signal) =>
        signal.sources.map((source) => source.messageId)
      );
      const sourceTexts = work.signals.flatMap((signal) =>
        signal.sources.map((source) => source.rawText)
      );
      return work.events
        .filter((event) => event.kind === "work.proposed")
        .map(
          (event): WorkNotice => ({
            createdAt: event.createdAt,
            eventId: event._id,
            sourceMessageIds,
            sourceTexts,
            workId: work._id,
          })
        );
    });
    return EffectArray.sortWith(
      notices,
      (notice) => notice.createdAt,
      Order.Number
    );
  }
);

export const projectWorkNotices = (input: unknown): readonly WorkNotice[] =>
  Effect.runSync(projectWorkNoticesEffect(input));
