---
Links context. The front door of the URL shortener.

Owns the link-storage agent and the HTTP API. Consumes the analytics context
to record a hit each time a link is resolved. Depends on a CodeGen capability
to mint new short codes.
---
context shortener.links

uses shortener.core
consumes shortener.analytics as Analytics

exports transparent { LinkError }

type LinkError = enum {
	AlreadyExists,
	NotFound,
}

---
Request body for creating a link. The `target` field is typed as the refined
Url, so the HTTP body deserialiser validates the URL shape and returns 400
before the handler runs if it is malformed.
---
type CreateLinkRequest = {
	target: Url,
}

---
The view returned when a link is created. Carries the refined ShortCode and
Url, which serialise to their underlying strings in the JSON response.
---
type CreatedView = {
	code:   ShortCode,
	target: Url,
}

---
The view returned when a link is resolved.
---
type ResolveView = {
	target: Url,
	hits:   Int,
}

---
Mints new short codes as raw strings. In production a provider would draw from
a random source or a counter; here the stub returns a fixed string. The links
domain validates the string into a ShortCode via `ShortCode.of(...)` at the use
site — refined values are always born from validation at a boundary.
---
capability CodeGen {
	fn next() -> Effect[String]
}

---
Stub provider: always returns the same code string.
---
provides CodeGen = FixedCodeGen {
	fn next() -> Effect[String] {
		"abc123"
	}
}

---
Stores the target URL for one short code. One agent instance per code.

State note: `target` is an Option, assumed to initialise to None for an unseen
code. The test "a fresh Link key resolves to NotFound" probes exactly this — if
the runtime does not initialise Option state to None, that probe is where it
surfaces (finding #10, the Option counterpart to the analytics Int probe).
---
agent Link {
	key code: ShortCode

	store target: Cell[Option[Url]]

	---
	Register a target URL for this code. Fails if the code is already taken.
	---
	on call register(url: Url) -> Effect[Result[(), LinkError]] {
		match target {
			Some(_) => Err(AlreadyExists)
			None    => {
				target := Some(url)
				Ok(())
			}
		}
	}

	---
	Resolve this code to its target URL. Fails if the code is unknown.
	---
	on call resolve() -> Effect[Result[Url, LinkError]] {
		match target {
			Some(url) => Ok(url)
			None      => Err(NotFound)
		}
	}
}

---
Internal RPC to create a link with an explicit code. Its own service, so the
operation is addressable as `create` (callable from tests, or from other
contexts if any consumed links).
---
service create {
	on call(code: ShortCode, target: Url) -> Effect[Result[(), LinkError]] {
		let link = Link(code)
		let outcome <- link.register(target)
		outcome
	}
}

---
The HTTP surface. A service with only `on http` handlers (no `on call`), so it
isn't cross-context callable — it faces external clients.
---
service api from http {
	---
	Create a shortened link. Mints a code, stores the target, and returns the
	created view. Domain errors map explicitly to HTTP outcomes.
	---
	on POST("/links") by v: Visitor (body: CreateLinkRequest) -> Effect[HttpResult[CreatedView]]
			given CodeGen {
		let raw <- CodeGen.next()
		match ShortCode.of(raw) {
			Err(_) => ServerError("generated an invalid code")
			Ok(code) => {
				let link = Link(code)
				let outcome <- link.register(body.target)

				match outcome {
					Ok(_) => Created(CreatedView { code: code, target: body.target })
					Err(linkError) => match linkError {
						AlreadyExists => Conflict("code already in use")
						NotFound      => ServerError("unexpected state")
					}
				}
			}
		}
	}

	---
	Resolve a short code to its target, recording a hit in the analytics
	context. The cross-context call to Analytics.track crosses a boundary:
	`code` is projected from links' ShortCode to analytics' ShortCode.
	---
	on GET("/links/:code") by v: Visitor (code: ShortCode) -> Effect[HttpResult[ResolveView]] {
		let link = Link(code)
		let outcome <- link.resolve()

		match outcome {
			Ok(url) => {
				-- Cross-context call. Analytics.track returns
				-- `Effect[Result[Int, AnalyticsError]]`, so unwrapping the
				-- effect leaves a Result that must be mapped at the HTTP
				-- boundary. A future version with boundary-error capture would
				-- let us treat the hit count as best-effort.
				let hitsResult <- Analytics.track(code)
				match hitsResult {
					Ok(hits) => Ok(ResolveView { target: url, hits: hits })
					Err(_)   => ServerError("analytics unavailable")
				}
			}
			Err(linkError) => match linkError {
				NotFound      => NotFound
				AlreadyExists => ServerError("unexpected state")
			}
		}
	}
}
