use pylon_kernel::AppManifest;

/// Map a manifest field type string to a TS type.
fn ts_type(field_type: &str) -> &str {
    match field_type {
        "string" => "string",
        "int" => "number",
        "float" => "number",
        "bool" => "boolean",
        "datetime" => "string",
        "richtext" => "string",
        _ if field_type.starts_with("id(") => "string",
        _ => "unknown",
    }
}

/// Generate a TypeScript client bindings file from a manifest.
pub fn generate_client_ts(manifest: &AppManifest) -> String {
    let mut out = String::new();

    out.push_str("// Generated by pylon codegen client. Do not edit.\n\n");

    // Entity names
    if !manifest.entities.is_empty() {
        let names: Vec<String> = manifest
            .entities
            .iter()
            .map(|e| format!("\"{}\"", e.name))
            .collect();
        out.push_str(&format!(
            "export type EntityName = {};\n\n",
            names.join(" | ")
        ));

        out.push_str(&format!(
            "export const ENTITIES: EntityName[] = [{}];\n\n",
            names.join(", ")
        ));
    }

    // Entity field interfaces
    for entity in &manifest.entities {
        out.push_str(&format!("export interface {} {{\n", entity.name));
        out.push_str("  id: string;\n");
        for field in &entity.fields {
            let ts = ts_type(&field.field_type);
            let optional = if field.optional { "?" } else { "" };
            out.push_str(&format!("  {}{}: {};\n", field.name, optional, ts));
        }
        out.push_str("}\n\n");
    }

    // Query names
    if !manifest.queries.is_empty() {
        let names: Vec<String> = manifest
            .queries
            .iter()
            .map(|q| format!("\"{}\"", q.name))
            .collect();
        out.push_str(&format!(
            "export type QueryName = {};\n\n",
            names.join(" | ")
        ));

        out.push_str(&format!(
            "export const QUERIES: QueryName[] = [{}];\n\n",
            names.join(", ")
        ));
    }

    // Query input interfaces
    for query in &manifest.queries {
        if !query.input.is_empty() {
            out.push_str(&format!(
                "export interface {}Input {{\n",
                capitalize(&query.name)
            ));
            for field in &query.input {
                let ts = ts_type(&field.field_type);
                let optional = if field.optional { "?" } else { "" };
                out.push_str(&format!("  {}{}: {};\n", field.name, optional, ts));
            }
            out.push_str("}\n\n");
        }
    }

    // Action names
    if !manifest.actions.is_empty() {
        let names: Vec<String> = manifest
            .actions
            .iter()
            .map(|a| format!("\"{}\"", a.name))
            .collect();
        out.push_str(&format!(
            "export type ActionName = {};\n\n",
            names.join(" | ")
        ));

        out.push_str(&format!(
            "export const ACTIONS: ActionName[] = [{}];\n\n",
            names.join(", ")
        ));
    }

    // Action input interfaces
    for action in &manifest.actions {
        if !action.input.is_empty() {
            out.push_str(&format!(
                "export interface {}Input {{\n",
                capitalize(&action.name)
            ));
            for field in &action.input {
                let ts = ts_type(&field.field_type);
                let optional = if field.optional { "?" } else { "" };
                out.push_str(&format!("  {}{}: {};\n", field.name, optional, ts));
            }
            out.push_str("}\n\n");
        }
    }

    // Runtime client
    out.push_str(
        "// ---------------------------------------------------------------------------\n",
    );
    out.push_str("// Runtime client\n");
    out.push_str(
        "// ---------------------------------------------------------------------------\n\n",
    );

    out.push_str("const DEFAULT_BASE_URL = \"http://localhost:4321\";\n\n");

    out.push_str("async function _request(baseUrl: string, method: string, path: string, body?: unknown, token?: string): Promise<unknown> {\n");
    out.push_str("  const headers: Record<string, string> = {};\n");
    out.push_str("  if (body) headers[\"Content-Type\"] = \"application/json\";\n");
    out.push_str("  if (token) headers[\"Authorization\"] = `Bearer ${token}`;\n");
    out.push_str("  const res = await fetch(`${baseUrl}${path}`, {\n");
    out.push_str("    method,\n");
    out.push_str("    headers,\n");
    out.push_str("    body: body ? JSON.stringify(body) : undefined,\n");
    out.push_str("  });\n");
    out.push_str("  if (!res.ok) {\n");
    out.push_str(
        "    const err = await res.json().catch(() => ({})) as Record<string, unknown>;\n",
    );
    out.push_str("    const errorObj = err?.error as Record<string, unknown> | undefined;\n");
    out.push_str("    throw new Error((errorObj?.message as string) ?? `HTTP ${res.status}`);\n");
    out.push_str("  }\n");
    out.push_str("  return res.json();\n");
    out.push_str("}\n\n");

    // Room types
    out.push_str(
        "// ---------------------------------------------------------------------------\n",
    );
    out.push_str("// Rooms\n");
    out.push_str(
        "// ---------------------------------------------------------------------------\n\n",
    );

    out.push_str("export interface RoomPeer {\n");
    out.push_str("  user_id: string;\n");
    out.push_str("  data: any;\n");
    out.push_str("  joined_at: string;\n");
    out.push_str("}\n\n");

    out.push_str("export interface RoomSnapshot {\n");
    out.push_str("  room: string;\n");
    out.push_str("  peers: RoomPeer[];\n");
    out.push_str("}\n\n");

    out.push_str("export interface RoomClient {\n");
    out.push_str("  join(room: string, data?: Record<string, any>): Promise<RoomSnapshot>;\n");
    out.push_str("  leave(room: string): Promise<void>;\n");
    out.push_str("  setPresence(room: string, data: Record<string, any>): Promise<void>;\n");
    out.push_str("  broadcast(room: string, topic: string, data: any): Promise<void>;\n");
    out.push_str("  getMembers(room: string): Promise<RoomPeer[]>;\n");
    out.push_str("  listRooms(): Promise<Array<{ name: string; members: number }>>;\n");
    out.push_str("}\n\n");

    // CRUD client
    out.push_str("export interface AgentDBClient {\n");
    out.push_str("  list(entity: EntityName): Promise<Record<string, unknown>[]>;\n");
    out.push_str(
        "  get(entity: EntityName, id: string): Promise<Record<string, unknown> | null>;\n",
    );
    out.push_str(
        "  insert(entity: EntityName, data: Record<string, unknown>): Promise<{ id: string }>;\n",
    );
    out.push_str("  update(entity: EntityName, id: string, data: Record<string, unknown>): Promise<{ updated: boolean }>;\n");
    out.push_str("  remove(entity: EntityName, id: string): Promise<{ deleted: boolean }>;\n");
    out.push_str(
        "  action(name: ActionName, input: Record<string, unknown>): Promise<ActionResult>;\n",
    );
    out.push_str("  actions: Actions;\n");
    out.push_str("  rooms: RoomClient;\n");
    out.push_str("}\n\n");

    out.push_str("export interface ActionResult {\n");
    out.push_str("  action: string;\n");
    out.push_str("  input: Record<string, unknown>;\n");
    out.push_str("  executed: boolean;\n");
    out.push_str("}\n\n");

    // Typed actions interface
    out.push_str("export interface Actions {\n");
    for action in &manifest.actions {
        let input_type = if action.input.is_empty() {
            "Record<string, never>".to_string()
        } else {
            format!("{}Input", capitalize(&action.name))
        };
        out.push_str(&format!(
            "  {}(input: {}): Promise<ActionResult>;\n",
            action.name, input_type
        ));
    }
    out.push_str("}\n\n");

    // RoomClient factory
    out.push_str("function _createRoomClient(baseUrl: string, token?: string): RoomClient {\n");
    out.push_str("  const req = (method: string, path: string, body?: unknown) => _request(baseUrl, method, path, body, token);\n");
    out.push_str("  return {\n");
    out.push_str("    async join(room, data = {}) {\n");
    out.push_str("      const res = await req(\"POST\", \"/api/rooms/join\", { room, data });\n");
    out.push_str("      return (res as any).snapshot as RoomSnapshot;\n");
    out.push_str("    },\n");
    out.push_str("    async leave(room) {\n");
    out.push_str("      await req(\"POST\", \"/api/rooms/leave\", { room });\n");
    out.push_str("    },\n");
    out.push_str("    async setPresence(room, data) {\n");
    out.push_str("      await req(\"POST\", \"/api/rooms/presence\", { room, data });\n");
    out.push_str("    },\n");
    out.push_str("    async broadcast(room, topic, data) {\n");
    out.push_str("      await req(\"POST\", \"/api/rooms/broadcast\", { room, topic, data });\n");
    out.push_str("    },\n");
    out.push_str("    async getMembers(room) {\n");
    out.push_str(
        "      const res = await req(\"GET\", `/api/rooms/${encodeURIComponent(room)}`);\n",
    );
    out.push_str("      return (res as any).members as RoomPeer[];\n");
    out.push_str("    },\n");
    out.push_str("    async listRooms() {\n");
    out.push_str("      const res = await req(\"GET\", \"/api/rooms\");\n");
    out.push_str("      return res as Array<{ name: string; members: number }>;\n");
    out.push_str("    },\n");
    out.push_str("  };\n");
    out.push_str("}\n\n");

    out.push_str("export function createClient(baseUrl: string = DEFAULT_BASE_URL, token?: string): AgentDBClient {\n");
    out.push_str("  const req = (method: string, path: string, body?: unknown) => _request(baseUrl, method, path, body, token);\n\n");

    // Typed actions object
    out.push_str("  const actions: Actions = {\n");
    for action in &manifest.actions {
        out.push_str(&format!(
            "    {name}: (input) => req(\"POST\", \"/api/actions/{name}\", input) as Promise<ActionResult>,\n",
            name = action.name
        ));
    }
    out.push_str("  };\n\n");

    out.push_str("  return {\n");
    out.push_str("    list: (entity) => req(\"GET\", `/api/entities/${entity}`) as Promise<Record<string, unknown>[]>,\n");
    out.push_str("    get: (entity, id) => req(\"GET\", `/api/entities/${entity}/${id}`).then(r => r as Record<string, unknown>).catch(() => null),\n");
    out.push_str("    insert: (entity, data) => req(\"POST\", `/api/entities/${entity}`, data) as Promise<{ id: string }>,\n");
    out.push_str("    update: (entity, id, data) => req(\"PATCH\", `/api/entities/${entity}/${id}`, data) as Promise<{ updated: boolean }>,\n");
    out.push_str("    remove: (entity, id) => req(\"DELETE\", `/api/entities/${entity}/${id}`) as Promise<{ deleted: boolean }>,\n");
    out.push_str("    action: (name, input) => req(\"POST\", `/api/actions/${name}`, input) as Promise<ActionResult>,\n");
    out.push_str("    actions,\n");
    out.push_str("    rooms: _createRoomClient(baseUrl, token),\n");
    out.push_str("  };\n");
    out.push_str("}\n\n");

    // Server Actions helpers (for Next.js App Router)
    out.push_str(
        "// ---------------------------------------------------------------------------\n",
    );
    out.push_str("// Server Actions (Next.js App Router compatible)\n");
    out.push_str(
        "// ---------------------------------------------------------------------------\n\n",
    );

    out.push_str("/** Create a server-side client for use in Next.js Server Actions / RSC. */\n");
    out.push_str("export function createServerClient(\n");
    out.push_str("  baseUrl: string = process.env.PYLON_URL ?? DEFAULT_BASE_URL,\n");
    out.push_str("  token?: string\n");
    out.push_str("): AgentDBClient {\n");
    out.push_str("  return createClient(baseUrl, token);\n");
    out.push_str("}\n\n");

    // React 19 use()-compatible data fetching
    out.push_str(
        "// ---------------------------------------------------------------------------\n",
    );
    out.push_str("// React 19 use() compatible promises\n");
    out.push_str(
        "// ---------------------------------------------------------------------------\n\n",
    );

    out.push_str("/** Pre-fetch data as a promise for React 19 use() hook. */\n");
    out.push_str("export function prefetch(client: AgentDBClient, entity: EntityName): Promise<Record<string, unknown>[]> {\n");
    out.push_str("  return client.list(entity);\n");
    out.push_str("}\n\n");

    out.push_str("/** Pre-fetch a single row for React 19 use() hook. */\n");
    out.push_str("export function prefetchOne(client: AgentDBClient, entity: EntityName, id: string): Promise<Record<string, unknown> | null> {\n");
    out.push_str("  return client.get(entity, id);\n");
    out.push_str("}\n\n");

    // ------------------------------------------------------------------
    // AppSchema — shape consumed by @pylon/react's createTypedDb<S>()
    // ------------------------------------------------------------------
    out.push_str(
        "// ---------------------------------------------------------------------------\n",
    );
    out.push_str("// AppSchema — types consumed by @pylon/react's createTypedDb<S>()\n");
    out.push_str(
        "// ---------------------------------------------------------------------------\n\n",
    );

    // Entities map.
    out.push_str("export interface Entities {\n");
    for entity in &manifest.entities {
        out.push_str(&format!("  {}: {};\n", entity.name, entity.name));
    }
    out.push_str("}\n\n");

    // Actions/functions map: { name: { input, output } }.
    out.push_str("export interface Functions {\n");
    for action in &manifest.actions {
        let input_type = if action.input.is_empty() {
            "Record<string, never>".to_string()
        } else {
            format!("{}Input", capitalize(&action.name))
        };
        out.push_str(&format!(
            "  {name}: {{ input: {input}; output: ActionResult }};\n",
            name = action.name,
            input = input_type
        ));
    }
    out.push_str("}\n\n");

    // Queries map.
    out.push_str("export interface Queries {\n");
    for query in &manifest.queries {
        let input_type = if query.input.is_empty() {
            "Record<string, never>".to_string()
        } else {
            format!("{}Input", capitalize(&query.name))
        };
        out.push_str(&format!(
            "  {name}: {{ input: {input}; output: unknown }};\n",
            name = query.name,
            input = input_type
        ));
    }
    out.push_str("}\n\n");

    out.push_str(
        "/** Top-level app schema — pass to @pylon/react's createTypedDb<AppSchema>(). */\n",
    );
    out.push_str("export interface AppSchema {\n");
    out.push_str("  entities: Entities;\n");
    out.push_str("  functions: Functions;\n");
    out.push_str("  queries: Queries;\n");
    out.push_str("}\n\n");

    // Error codes — typed union so clients can exhaustively match.
    out.push_str(
        "// ---------------------------------------------------------------------------\n",
    );
    out.push_str("// Error codes\n");
    out.push_str(
        "// ---------------------------------------------------------------------------\n\n",
    );
    let codes: Vec<String> = pylon_kernel::errors::ALL_CODES
        .iter()
        .map(|c| format!("  | \"{c}\""))
        .collect();
    out.push_str(&format!(
        "export type AgentDBErrorCode =\n{};\n\n",
        codes.join("\n")
    ));
    out.push_str("export interface AgentDBError {\n");
    out.push_str("  code: AgentDBErrorCode;\n");
    out.push_str("  message: string;\n");
    out.push_str("  hint?: string;\n");
    out.push_str("}\n");

    out
}

fn capitalize(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_uppercase().to_string() + chars.as_str(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pylon_kernel::{
        ManifestAction, ManifestEntity, ManifestField, ManifestIndex, ManifestQuery,
    };

    // Synthesizes a Todo-style manifest in memory rather than reading
    // examples/todo-app/pylon.manifest.json. Pinning these assertions
    // to a real example produced false failures every time the example
    // was simplified or restructured. The shape here matches what the
    // SDK's buildManifest emits, so we still exercise the full codegen
    // path against realistic User+Todo entities, two queries, and two
    // actions.
    fn todo_manifest() -> AppManifest {
        fn f(name: &str, ty: &str, optional: bool) -> ManifestField {
            ManifestField {
                name: name.into(),
                field_type: ty.into(),
                optional,
                unique: false,
                crdt: None,
            }
        }
        AppManifest {
            manifest_version: 1,
            name: "todo-app".into(),
            version: "0.1.0".into(),
            routes: vec![],
            policies: vec![],
            entities: vec![
                ManifestEntity {
                    name: "User".into(),
                    fields: vec![f("email", "string", false)],
                    indexes: vec![],
                    relations: vec![],
                    search: None,
                    crdt: true,
                },
                ManifestEntity {
                    name: "Todo".into(),
                    fields: vec![
                        f("title", "string", false),
                        f("done", "bool", false),
                        f("authorId", "id(User)", false),
                    ],
                    indexes: vec![ManifestIndex {
                        name: "by_author".into(),
                        fields: vec!["authorId".into()],
                        unique: false,
                    }],
                    relations: vec![],
                    search: None,
                    crdt: true,
                },
            ],
            queries: vec![
                ManifestQuery {
                    name: "todosByAuthor".into(),
                    input: vec![f("authorId", "id(User)", false)],
                },
                ManifestQuery {
                    name: "allTodos".into(),
                    input: vec![f("done", "bool", true)],
                },
                ManifestQuery {
                    name: "todoById".into(),
                    input: vec![f("id", "id(Todo)", false)],
                },
            ],
            actions: vec![
                ManifestAction {
                    name: "createTodo".into(),
                    input: vec![
                        f("title", "string", false),
                        f("authorId", "id(User)", false),
                    ],
                },
                ManifestAction {
                    name: "toggleTodo".into(),
                    input: vec![f("todoId", "id(Todo)", false)],
                },
            ],
            auth: Default::default(),
        }
    }

    #[test]
    fn generates_entity_names() {
        let ts = generate_client_ts(&todo_manifest());
        assert!(ts.contains("export type EntityName = \"User\" | \"Todo\";"));
        assert!(ts.contains("export const ENTITIES: EntityName[] = [\"User\", \"Todo\"];"));
    }

    #[test]
    fn generates_entity_interfaces() {
        let ts = generate_client_ts(&todo_manifest());
        assert!(ts.contains("export interface User {"));
        assert!(ts.contains("  id: string;"));
        assert!(ts.contains("  email: string;"));
        assert!(ts.contains("export interface Todo {"));
        assert!(ts.contains("  done: boolean;"));
        assert!(ts.contains("  authorId: string;")); // id(User) -> string
    }

    #[test]
    fn generates_query_names() {
        let ts = generate_client_ts(&todo_manifest());
        assert!(ts.contains("export type QueryName ="));
        assert!(ts.contains("\"todosByAuthor\""));
        assert!(ts.contains("\"allTodos\""));
        assert!(ts.contains("\"todoById\""));
    }

    #[test]
    fn generates_query_input_interfaces() {
        let ts = generate_client_ts(&todo_manifest());
        assert!(ts.contains("export interface TodosByAuthorInput {"));
        assert!(ts.contains("  authorId: string;"));
        assert!(ts.contains("export interface AllTodosInput {"));
        assert!(ts.contains("  done?: boolean;")); // optional
    }

    #[test]
    fn generates_action_names() {
        let ts = generate_client_ts(&todo_manifest());
        assert!(ts.contains("export type ActionName ="));
        assert!(ts.contains("\"createTodo\""));
        assert!(ts.contains("\"toggleTodo\""));
    }

    #[test]
    fn generates_action_input_interfaces() {
        let ts = generate_client_ts(&todo_manifest());
        assert!(ts.contains("export interface CreateTodoInput {"));
        assert!(ts.contains("  title: string;"));
        assert!(ts.contains("export interface ToggleTodoInput {"));
        assert!(ts.contains("  todoId: string;"));
    }

    #[test]
    fn generates_room_types() {
        let ts = generate_client_ts(&todo_manifest());
        assert!(ts.contains("export interface RoomPeer {"));
        assert!(ts.contains("  user_id: string;"));
        assert!(ts.contains("export interface RoomSnapshot {"));
        assert!(ts.contains("export interface RoomClient {"));
        assert!(ts.contains("  rooms: RoomClient;"));
    }

    #[test]
    fn generates_room_client_implementation() {
        let ts = generate_client_ts(&todo_manifest());
        assert!(ts.contains("function _createRoomClient("));
        assert!(ts.contains("rooms: _createRoomClient(baseUrl, token),"));
    }

    #[test]
    fn generates_app_schema() {
        let ts = generate_client_ts(&todo_manifest());
        assert!(ts.contains("export interface Entities {"));
        assert!(ts.contains("  User: User;"));
        assert!(ts.contains("  Todo: Todo;"));
        assert!(ts.contains("export interface Functions {"));
        assert!(ts.contains("createTodo: { input: CreateTodoInput; output: ActionResult }"));
        assert!(ts.contains("export interface Queries {"));
        assert!(ts.contains("export interface AppSchema {"));
        assert!(ts.contains("entities: Entities;"));
        assert!(ts.contains("functions: Functions;"));
        assert!(ts.contains("queries: Queries;"));
    }

    #[test]
    fn generates_empty_function_input() {
        // Actions with zero inputs should use Record<string, never>.
        let ts = generate_client_ts(&todo_manifest());
        // toggleTodo has 1 input so it should have TypedInput.
        // Any hypothetical empty-input action would get Record<string, never>.
        // Just verify the format is consistent.
        assert!(ts.contains("input: "));
        assert!(ts.contains("output: ActionResult"));
    }

    #[test]
    fn output_is_deterministic() {
        let m = todo_manifest();
        let a = generate_client_ts(&m);
        let b = generate_client_ts(&m);
        assert_eq!(a, b);
    }

    #[test]
    fn ts_type_mapping() {
        assert_eq!(ts_type("string"), "string");
        assert_eq!(ts_type("int"), "number");
        assert_eq!(ts_type("float"), "number");
        assert_eq!(ts_type("bool"), "boolean");
        assert_eq!(ts_type("datetime"), "string");
        assert_eq!(ts_type("richtext"), "string");
        assert_eq!(ts_type("id(User)"), "string");
        assert_eq!(ts_type("nonsense"), "unknown");
    }
}
