use serde_json::{json, Map, Value};
use syncular_runtime::app_schema::{
    unknown_table_adapter, AppSchema, AppTableMetadata, ColumnMetadata, EmbeddedMigration,
    ScopeMetadata, ScopeSource,
};
use syncular_runtime::client::{SubscriptionSpec, SyncularClientConfig};
use syncular_runtime::error::Result;
use syncular_runtime::protocol::{
    BlobRef, IntoSyncularMutation, PendingSyncularMutation, SyncularMutationKind,
};
use syncular_runtime::transport::SyncTransport;

use crate::app::{
    open_app_client_with_options, open_app_client_with_server, open_app_client_with_transport,
    AppFixture, AppFixtureOptions, TestAppFixture,
};
use crate::app_server::AppTestServer;

pub const FILES_TABLE: &str = "files";
pub const FILE_VERSIONS_TABLE: &str = "file_versions";
pub const FILES_SUBSCRIPTION_ID: &str = "sub-files";
pub const FILE_VERSIONS_SUBSCRIPTION_ID: &str = "sub-file-versions";

const FILE_ASSET_MIGRATION_SQL: &str = r#"
CREATE TABLE IF NOT EXISTS files (
  id TEXT PRIMARY KEY,
  parent_id TEXT NULL,
  name TEXT NOT NULL,
  kind TEXT NOT NULL,
  current_version_id TEXT NULL,
  owner_id TEXT NOT NULL,
  project_id TEXT NULL,
  deleted INTEGER NOT NULL DEFAULT 0,
  trashed_at BIGINT NULL,
  server_version BIGINT NOT NULL DEFAULT 0
) WITHOUT ROWID;

CREATE TABLE IF NOT EXISTS file_versions (
  id TEXT PRIMARY KEY,
  file_id TEXT NOT NULL,
  blob_ref TEXT NOT NULL,
  content_hash TEXT NOT NULL,
  byte_size BIGINT NOT NULL,
  mime_type TEXT NULL,
  actor_id TEXT NOT NULL,
  owner_id TEXT NOT NULL,
  project_id TEXT NULL,
  previous_version_id TEXT NULL,
  created_at BIGINT NOT NULL,
  server_version BIGINT NOT NULL DEFAULT 0
) WITHOUT ROWID;

CREATE INDEX IF NOT EXISTS idx_files_scope_parent_name
  ON files (owner_id, project_id, parent_id, name);

CREATE INDEX IF NOT EXISTS idx_file_versions_file_created
  ON file_versions (file_id, created_at);
"#;

const FILE_ASSET_MIGRATIONS: &[EmbeddedMigration] = &[EmbeddedMigration {
    version: "0001",
    schema_version: 1,
    name: "file_asset_reference_schema",
    up_sql: FILE_ASSET_MIGRATION_SQL,
}];

const FILE_ASSET_TABLES: &[&str] = &[FILES_TABLE, FILE_VERSIONS_TABLE];

const FILES_COLUMNS: &[ColumnMetadata] = &[
    ColumnMetadata {
        name: "id",
        type_family: "text",
        notnull_required: false,
        primary_key: true,
    },
    ColumnMetadata {
        name: "parent_id",
        type_family: "text",
        notnull_required: false,
        primary_key: false,
    },
    ColumnMetadata {
        name: "name",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "kind",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "current_version_id",
        type_family: "text",
        notnull_required: false,
        primary_key: false,
    },
    ColumnMetadata {
        name: "owner_id",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "project_id",
        type_family: "text",
        notnull_required: false,
        primary_key: false,
    },
    ColumnMetadata {
        name: "deleted",
        type_family: "integer",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "trashed_at",
        type_family: "integer",
        notnull_required: false,
        primary_key: false,
    },
    ColumnMetadata {
        name: "server_version",
        type_family: "integer",
        notnull_required: true,
        primary_key: false,
    },
];

const FILE_VERSIONS_COLUMNS: &[ColumnMetadata] = &[
    ColumnMetadata {
        name: "id",
        type_family: "text",
        notnull_required: false,
        primary_key: true,
    },
    ColumnMetadata {
        name: "file_id",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "blob_ref",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "content_hash",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "byte_size",
        type_family: "integer",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "mime_type",
        type_family: "text",
        notnull_required: false,
        primary_key: false,
    },
    ColumnMetadata {
        name: "actor_id",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "owner_id",
        type_family: "text",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "project_id",
        type_family: "text",
        notnull_required: false,
        primary_key: false,
    },
    ColumnMetadata {
        name: "previous_version_id",
        type_family: "text",
        notnull_required: false,
        primary_key: false,
    },
    ColumnMetadata {
        name: "created_at",
        type_family: "integer",
        notnull_required: true,
        primary_key: false,
    },
    ColumnMetadata {
        name: "server_version",
        type_family: "integer",
        notnull_required: true,
        primary_key: false,
    },
];

const FILE_ASSET_SCOPES: &[ScopeMetadata] = &[
    ScopeMetadata {
        name: "user_id",
        column: "owner_id",
        source: ScopeSource::ActorId,
        required: true,
    },
    ScopeMetadata {
        name: "project_id",
        column: "project_id",
        source: ScopeSource::ProjectId,
        required: false,
    },
];

const FILES_METADATA: AppTableMetadata = AppTableMetadata {
    name: FILES_TABLE,
    primary_key_column: "id",
    server_version_column: "server_version",
    soft_delete_column: Some("deleted"),
    subscription_id: FILES_SUBSCRIPTION_ID,
    columns: FILES_COLUMNS,
    blob_columns: &[],
    crdt_yjs_fields: &[],
    encrypted_fields: &[],
    scopes: FILE_ASSET_SCOPES,
};

const FILE_VERSIONS_METADATA: AppTableMetadata = AppTableMetadata {
    name: FILE_VERSIONS_TABLE,
    primary_key_column: "id",
    server_version_column: "server_version",
    soft_delete_column: None,
    subscription_id: FILE_VERSIONS_SUBSCRIPTION_ID,
    columns: FILE_VERSIONS_COLUMNS,
    blob_columns: &["blob_ref"],
    crdt_yjs_fields: &[],
    encrypted_fields: &[],
    scopes: FILE_ASSET_SCOPES,
};

const FILE_ASSET_METADATA: &[AppTableMetadata] = &[FILES_METADATA, FILE_VERSIONS_METADATA];

pub type FileAssetFixture<T> = AppFixture<T>;
pub type TestFileAssetFixture = TestAppFixture;

pub fn file_asset_app_schema() -> AppSchema {
    AppSchema {
        app_tables: FILE_ASSET_TABLES,
        app_table_metadata: FILE_ASSET_METADATA,
        migrations: FILE_ASSET_MIGRATIONS,
        schema_version: Some(1),
        default_subscriptions: default_file_asset_subscriptions,
        adapter_for: unknown_table_adapter,
    }
}

pub fn open_file_asset_client() -> Result<TestFileAssetFixture> {
    open_file_asset_client_with_options(AppFixtureOptions {
        db_prefix: "syncular-file-assets-test".to_string(),
        ..AppFixtureOptions::default()
    })
}

pub fn open_file_asset_client_with_options(
    options: AppFixtureOptions,
) -> Result<TestFileAssetFixture> {
    open_app_client_with_options(file_asset_app_schema(), file_asset_fixture_options(options))
}

pub fn open_file_asset_client_with_transport<T>(
    transport: T,
    options: AppFixtureOptions,
) -> Result<FileAssetFixture<T>>
where
    T: SyncTransport,
{
    open_app_client_with_transport(
        file_asset_app_schema(),
        transport,
        file_asset_fixture_options(options),
    )
}

pub fn open_file_asset_client_with_server(
    server: AppTestServer,
    options: AppFixtureOptions,
) -> Result<FileAssetFixture<AppTestServer>> {
    open_app_client_with_server(
        file_asset_app_schema(),
        server,
        file_asset_fixture_options(options),
    )
}

fn file_asset_fixture_options(mut options: AppFixtureOptions) -> AppFixtureOptions {
    if options.db_prefix == AppFixtureOptions::default().db_prefix {
        options.db_prefix = "syncular-file-assets-test".to_string();
    }
    options
}

fn default_file_asset_subscriptions(config: &SyncularClientConfig) -> Vec<SubscriptionSpec> {
    let mut scopes = Map::new();
    scopes.insert(
        "user_id".to_string(),
        Value::String(config.actor_id.clone()),
    );
    if let Some(project_id) = &config.project_id {
        scopes.insert("project_id".to_string(), Value::String(project_id.clone()));
    }
    vec![
        SubscriptionSpec {
            id: FILES_SUBSCRIPTION_ID.to_string(),
            table: FILES_TABLE.to_string(),
            scopes: scopes.clone(),
            params: Map::new(),
            bootstrap_phase: 0,
        },
        SubscriptionSpec {
            id: FILE_VERSIONS_SUBSCRIPTION_ID.to_string(),
            table: FILE_VERSIONS_TABLE.to_string(),
            scopes,
            params: Map::new(),
            bootstrap_phase: 0,
        },
    ]
}

#[derive(Debug, Clone)]
pub struct NewFileAsset {
    id: String,
    parent_id: Option<String>,
    name: String,
    kind: String,
    current_version_id: Option<String>,
    owner_id: String,
    project_id: Option<String>,
}

impl NewFileAsset {
    pub fn file(id: &str, name: &str, owner_id: &str, project_id: Option<&str>) -> Self {
        Self::new(id, name, "file", owner_id, project_id)
    }

    pub fn folder(id: &str, name: &str, owner_id: &str, project_id: Option<&str>) -> Self {
        Self::new(id, name, "folder", owner_id, project_id)
    }

    fn new(id: &str, name: &str, kind: &str, owner_id: &str, project_id: Option<&str>) -> Self {
        Self {
            id: id.to_string(),
            parent_id: None,
            name: name.to_string(),
            kind: kind.to_string(),
            current_version_id: None,
            owner_id: owner_id.to_string(),
            project_id: project_id.map(str::to_string),
        }
    }

    pub fn parent_id(mut self, parent_id: Option<&str>) -> Self {
        self.parent_id = parent_id.map(str::to_string);
        self
    }

    pub fn current_version_id(mut self, version_id: Option<&str>) -> Self {
        self.current_version_id = version_id.map(str::to_string);
        self
    }

    pub fn row_json(&self) -> Value {
        let mut row = Map::new();
        row.insert("id".to_string(), json!(&self.id));
        row.insert("parent_id".to_string(), json!(&self.parent_id));
        row.insert("name".to_string(), json!(&self.name));
        row.insert("kind".to_string(), json!(&self.kind));
        row.insert(
            "current_version_id".to_string(),
            json!(&self.current_version_id),
        );
        row.insert("owner_id".to_string(), json!(&self.owner_id));
        row.insert("project_id".to_string(), json!(&self.project_id));
        row.insert("deleted".to_string(), json!(0));
        row.insert("trashed_at".to_string(), Value::Null);
        Value::Object(row)
    }

    fn payload_json(&self) -> Value {
        self.row_json()
    }
}

impl IntoSyncularMutation for NewFileAsset {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        PendingSyncularMutation {
            kind: SyncularMutationKind::Insert,
            table: FILES_TABLE.to_string(),
            row_id: self.id.clone(),
            payload: Some(self.payload_json()),
            base_version: None,
            local_row: Some(self.row_json()),
        }
    }
}

#[derive(Debug, Clone)]
pub struct FileAssetPatch {
    row_id: String,
    base_version: Option<i64>,
    parent_id: Option<Option<String>>,
    name: Option<String>,
    current_version_id: Option<Option<String>>,
    deleted: Option<i32>,
    trashed_at: Option<Option<i64>>,
}

impl FileAssetPatch {
    pub fn new(row_id: &str) -> Self {
        Self {
            row_id: row_id.to_string(),
            base_version: None,
            parent_id: None,
            name: None,
            current_version_id: None,
            deleted: None,
            trashed_at: None,
        }
    }

    pub fn base_version(mut self, base_version: i64) -> Self {
        self.base_version = Some(base_version);
        self
    }

    pub fn rename(mut self, name: &str) -> Self {
        self.name = Some(name.to_string());
        self
    }

    pub fn move_to(mut self, parent_id: Option<&str>) -> Self {
        self.parent_id = Some(parent_id.map(str::to_string));
        self
    }

    pub fn current_version_id(mut self, version_id: Option<&str>) -> Self {
        self.current_version_id = Some(version_id.map(str::to_string));
        self
    }

    pub fn soft_delete(mut self, trashed_at: i64) -> Self {
        self.deleted = Some(1);
        self.trashed_at = Some(Some(trashed_at));
        self
    }

    pub fn restore(mut self) -> Self {
        self.deleted = Some(0);
        self.trashed_at = Some(None);
        self
    }

    pub fn payload_json(&self) -> Value {
        let mut payload = Map::new();
        if let Some(parent_id) = &self.parent_id {
            payload.insert("parent_id".to_string(), json!(parent_id));
        }
        if let Some(name) = &self.name {
            payload.insert("name".to_string(), json!(name));
        }
        if let Some(version_id) = &self.current_version_id {
            payload.insert("current_version_id".to_string(), json!(version_id));
        }
        if let Some(deleted) = self.deleted {
            payload.insert("deleted".to_string(), json!(deleted));
        }
        if let Some(trashed_at) = self.trashed_at {
            payload.insert("trashed_at".to_string(), json!(trashed_at));
        }
        Value::Object(payload)
    }
}

impl IntoSyncularMutation for FileAssetPatch {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        let payload = self.payload_json();
        PendingSyncularMutation {
            kind: SyncularMutationKind::Update,
            table: FILES_TABLE.to_string(),
            row_id: self.row_id,
            payload: Some(payload),
            base_version: self.base_version,
            local_row: None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct FileAssetHardDelete {
    row_id: String,
    base_version: Option<i64>,
}

impl FileAssetHardDelete {
    pub fn new(row_id: &str) -> Self {
        Self {
            row_id: row_id.to_string(),
            base_version: None,
        }
    }

    pub fn base_version(mut self, base_version: i64) -> Self {
        self.base_version = Some(base_version);
        self
    }
}

impl IntoSyncularMutation for FileAssetHardDelete {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        PendingSyncularMutation {
            kind: SyncularMutationKind::Delete,
            table: FILES_TABLE.to_string(),
            row_id: self.row_id,
            payload: None,
            base_version: self.base_version,
            local_row: None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct NewFileVersion {
    id: String,
    file_id: String,
    blob_ref: BlobRef,
    actor_id: String,
    owner_id: String,
    project_id: Option<String>,
    previous_version_id: Option<String>,
    created_at: i64,
}

impl NewFileVersion {
    pub fn new(
        id: &str,
        file_id: &str,
        blob_ref: BlobRef,
        actor_id: &str,
        owner_id: &str,
        project_id: Option<&str>,
        created_at: i64,
    ) -> Self {
        Self {
            id: id.to_string(),
            file_id: file_id.to_string(),
            blob_ref,
            actor_id: actor_id.to_string(),
            owner_id: owner_id.to_string(),
            project_id: project_id.map(str::to_string),
            previous_version_id: None,
            created_at,
        }
    }

    pub fn previous_version_id(mut self, version_id: Option<&str>) -> Self {
        self.previous_version_id = version_id.map(str::to_string);
        self
    }

    pub fn row_json(&self) -> Value {
        let mut row = Map::new();
        row.insert("id".to_string(), json!(&self.id));
        row.insert("file_id".to_string(), json!(&self.file_id));
        row.insert("blob_ref".to_string(), json!(&self.blob_ref));
        row.insert("content_hash".to_string(), json!(&self.blob_ref.hash));
        row.insert("byte_size".to_string(), json!(self.blob_ref.size));
        row.insert("mime_type".to_string(), json!(&self.blob_ref.mime_type));
        row.insert("actor_id".to_string(), json!(&self.actor_id));
        row.insert("owner_id".to_string(), json!(&self.owner_id));
        row.insert("project_id".to_string(), json!(&self.project_id));
        row.insert(
            "previous_version_id".to_string(),
            json!(&self.previous_version_id),
        );
        row.insert("created_at".to_string(), json!(self.created_at));
        Value::Object(row)
    }

    fn payload_json(&self) -> Value {
        self.row_json()
    }
}

impl IntoSyncularMutation for NewFileVersion {
    fn into_syncular_mutation(self) -> PendingSyncularMutation {
        PendingSyncularMutation {
            kind: SyncularMutationKind::Insert,
            table: FILE_VERSIONS_TABLE.to_string(),
            row_id: self.id.clone(),
            payload: Some(self.payload_json()),
            base_version: None,
            local_row: Some(self.row_json()),
        }
    }
}
