import {
  ApplicationUserDto,
  BaseBlock,
  BranchDto,
  CommitDto,
  DatasourceEnvironments,
  DataTreeGroup,
  DataTreeUser,
  DegradedMode,
  Difference,
  ExportedApiV3Dto,
  IApiV3Dto,
  Plugin,
  RepositoryDto,
  SupersetIntegrationDto
} from '..';
import { ApplicationSignatureTreeSigned, Signature } from '../../signing/index.js';
import { ResourceMetadata } from '../metadata/index.js';
import { Dimension, EntityWithBindings6, IPageV2Dto, MultiStepDef, Padding, PageDSL, PageSummary } from '../page/index.js';
import { EntityProfileSettings } from '../profile/index.js';
import { IResourceMetadata } from '../resource/index.js';

export interface Application {
  id: string;
  environment: DatasourceEnvironments;
  name: string;
  // Older apps have this field, but it's unused
  icon?: string;
  color: string;
  organizationId: string;
  new: boolean;
  appIsExample: boolean;
  isPublic: boolean;
  userPermissions: string[];
  deletedAt: Date | undefined;
  settings: ApplicationSettings;
  configuration?: ApplicationConfiguration;
}

export type GetEditorsForAppIdResponse = ApplicationUserDto[];

interface PropertiesPanelDisplay {
  controlType: 'text' | 'js-expr' | 'switch';
  label: string;
  defaultValue?: string | number | boolean;
  placeholder?: string;
  exampleData?: string;
  expectedType?: string;
}

/** @pattern ^[a-zA-Z_][a-zA-Z0-9_]*$ */
type Identifier = string;

export interface CustomComponentProperty {
  path: Identifier;
  dataType: 'string' | 'number' | 'boolean' | 'any';
  propertiesPanelDisplay?: PropertiesPanelDisplay;
  description?: string;
  // Is the property settable by other components/triggers
  isExternallySettable?: boolean;
  // Is the property readable by other components/triggers/apis
  isExternallyReadable?: boolean;
}

// Important: These configs must be self-contained with no references to types that might be modified later
// because they are snapshots of a point in time
export interface PreviousCustomComponentConfigs {
  '0.0.20': {
    id: string;
    name: string;
    displayName: string;
    componentPath: string;
    statefulProperties: Array<{
      path: string;
      label: string;
      inputType: 'text' | 'number' | 'boolean' | 'js';
      placeholder?: string;
    }>;
    eventHandlers: Array<{ label: string; path: string }>;
  };
  '0.0.21': {
    // @format uuid
    id: string;
    name: string;
    displayName: string;
    componentPath: string;
    properties: Array<{
      path: string;
      dataType: 'string' | 'number' | 'boolean' | 'any';
      propertiesPanelDisplay?: {
        controlType: 'text' | 'js-expr' | 'switch';
        label: string;
        defaultValue?: string;
        placeholder?: string;
        exampleData?: string;
      };
      description?: string;
    }>;
    events: Array<{ label: string; path: string }>;
  };
}

export interface CustomComponentConfig {
  // @format uuid
  id: string;
  // Example: "myComponent"
  name: string;
  // Example: "My Component"
  displayName: string;
  // Example: "components/myComponent/component.tsx"
  componentPath: string;
  properties: CustomComponentProperty[];
  events: Array<{ label: string; path: Identifier }>;
  gridDimensions?: {
    initialColumns: number;
    initialRows: number;
  };
}

export enum ComponentEvent {
  CREATE = 'create',
  INIT = 'init',
  LOGIN = 'login',
  MIGRATE = 'migrate',
  PULL = 'pull',
  PUSH = 'push',
  REGISTER = 'register',
  UPLOAD = 'upload',
  DEPLOY = 'deploy'
}

export type RegisteredComponents = Record<string, CustomComponentConfig>;

// Defines a Route. Can be thought of as a route "template".
type BaseRoute = {
  id: string;
  path: string;
  testParams?: Record<string, string>;
  onRouteLoad?: MultiStepDef;
} & EntityWithBindings6;
export type PageRoute = BaseRoute & { pageId: string };
type PageComponentRoute = PageRoute & { widgetId: string };
type RedirectRoute = BaseRoute;
export type RouteDef = PageRoute | PageComponentRoute | RedirectRoute;

// Necessary because the fallback route might not be saved until the user first modifies their routes
export const DEFAULT_ROUTE_ID = 'ahjjxoy521';

// Necessary for backwards compatibility with pre-multi-page applications
export function getDefaultRoute(pageId: string): RouteDef {
  return {
    id: DEFAULT_ROUTE_ID,
    pageId,
    path: '/'
  };
}

export interface ApplicationConfiguration {
  // if version is not provided, it's assumed to be 1
  version?: 1;
  routes?: Record<string, RouteDef>;
  dsl?: {
    version: 8; // needs to match the current PageDSL version, which is 8 as of 7/10/24
    stateVars?: PageDSL['stateVars'];
    timers?: PageDSL['timers'];
    events?: PageDSL['events'];
    apis?: PageDSL['apis'];
  };
}

export interface ApplicationConfigurationUpdate extends ApplicationConfiguration {
  routes: Record<string, RouteDef>;
}

export enum ThemeMode {
  LIGHT = 'LIGHT',
  DARK = 'DARK',
  AUTO = 'AUTO'
}

export type Border = {
  color?: string;
  width?: Dimension<'px'>;
  style?: 'solid';
};

export type PerSideBorder = {
  left: Border;
  right: Border;
  top: Border;
  bottom: Border;
};

export type PerCornerBorderRadius = {
  topLeft: Dimension<'px'>;
  topRight: Dimension<'px'>;
  bottomLeft: Dimension<'px'>;
  bottomRight: Dimension<'px'>;
};

export type UserDefinedPalette = {
  primaryColor: string;
  appBackgroundColor: string;
};
export interface ColorBlock {
  default: string;
  disabled?: string;
  hover?: string;
  error?: string;
  active?: string;
  activeHover?: string;
}
interface TextColorBlock {
  textColor: ColorBlock;
}

interface BackgroundColorBlock {
  backgroundColor: ColorBlock;
}

interface BorderColorBlock {
  borderColor: ColorBlock;
}

interface BoxShadowBlock {
  boxShadow: ColorBlock;
}

export interface IconColorBlock {
  iconColor: ColorBlock;
}

interface SizeBlock {
  height: Dimension;
  width: Dimension;
}

export interface ColoredDimensionStyle {
  size: Dimension;
  color?: string;
}

export interface BorderStyleBlock extends BorderColorBlock, BoxShadowBlock {
  borderWidth: Dimension;
  borderStyle: string;
  borderRadius: Dimension;
}

export interface StyleBlockWithSize extends StyleBlock, SizeBlock {}
export interface TextStyleBlock extends TextColorBlock {
  fontFamily: string;
  fontSize: string;
  fontWeight: number;
  lineHeight: number | string;
  textDecoration?: string;
  fontStyle?: 'normal' | 'italic' | 'inherit';
  letterSpacing?: string;
  textTransform?: 'uppercase' | 'lowercase' | 'capitalize' | 'none' | 'inherit';
}

export type CustomTypographies = Record<
  string,
  {
    styles: TextStyleBlock;
    name: string;
    key: string;
  }
>;

export interface StyleBlock extends TextStyleBlock, BorderStyleBlock, BackgroundColorBlock {
  padding: Padding;
}

export const CUSTOM_THEME_TYPOGRAPHY_KEY = 'custom';

export type Typographies = {
  heading1: TextStyleBlock;
  heading2: TextStyleBlock;
  heading3: TextStyleBlock;
  heading4: TextStyleBlock;
  heading5: TextStyleBlock;
  heading6: TextStyleBlock;
  body1: TextStyleBlock;
  body2: TextStyleBlock;
  body3: TextStyleBlock;
  label: TextStyleBlock;
  inputLabel: TextStyleBlock;
  inputPlaceholder: TextStyleBlock;
  inputText: TextStyleBlock;
  buttonLabel: TextStyleBlock;
  link: TextStyleBlock;
  code: TextStyleBlock;
  [CUSTOM_THEME_TYPOGRAPHY_KEY]?: CustomTypographies;
};

export type NonCustomTypography = Exclude<keyof Typographies, typeof CUSTOM_THEME_TYPOGRAPHY_KEY>;

export type FontFamily = {
  name: string;
  key: string;
  url?: string;
  type: 'google' | 'custom';
  weights?: Array<number>;
};

export type UserDefinedTheme = {
  primaryColor: string;
  mode: ThemeMode;
  borderRadius: Dimension<'px'>;
  typeFace: string;
  // optional because themes were released without padding
  padding?: Padding;
  palette?: {
    dark?: UserDefinedPalette;
    light?: UserDefinedPalette;
  };
  typographies?: Typographies; // optional: themes were released without typographies
  availableFonts?: Record<string, FontFamily>;
  version?: number; // optional: themes were released without versioning
};

export type ApplicationSettings = EntityProfileSettings & VersionedApplicationSettings;

export type NonVersionedApplicationSettings = Difference<ApplicationSettings, ExportedApplicationSettings>;
export type VersionedApplicationSettings = {
  // CDN prefix for components
  componentBaseUrl?: string;
  // Custom component definitions
  registeredComponents?: RegisteredComponents;
  // Array of source file paths for custom components
  // @deprecated
  files?: string[];
  // Added by file upload service
  sourceFiles?: string[];
  bundledFiles?: string[];
  // Version of the CLI used to build the custom components
  // it's using a semver, which can't regress
  cliVersion?: string;
  // Timestamp of the most recent upload
  componentsLastUploaded?: string;
  theme?: UserDefinedTheme;
};

export function toExportedApplicationSettings(applicationSettings: ApplicationSettings): ExportedApplicationSettings {
  return {
    componentBaseUrl: applicationSettings.componentBaseUrl,
    registeredComponents: applicationSettings.registeredComponents,
    files: applicationSettings.files,
    sourceFiles: applicationSettings.sourceFiles,
    bundledFiles: applicationSettings.bundledFiles,
    cliVersion: applicationSettings.cliVersion,
    componentsLastUploaded: applicationSettings.componentsLastUploaded,
    theme: applicationSettings.theme
  };
}

export function toNonVersionedApplicationSettings(applicationSettings?: ApplicationSettings): NonVersionedApplicationSettings | undefined {
  if (!applicationSettings) {
    return;
  }
  return {
    profiles: applicationSettings.profiles
  };
}

export type ApplicationSettingsResponse = ApplicationSettings & {
  prefix?: string;
};

export interface FullApplicationV2Dto {
  application: IApplicationV2Dto;
  metadata?: ResourceMetadata;
  plugins?: Plugin[];
  integrations?: SupersetIntegrationDto[];
  page?: IPageV2Dto | null;
  // @deprecated this is no longer used and currently not even populated in the responses
  editors?: ApplicationUserDto[];
  global?: Partial<ApplicationV2GlobalDto>;
  apis?: IApiV3Dto[];
  versions?: {
    current: CommitDto;
  }; // for backwards compatibility
}

export type ExportedApplication = Omit<
  FullApplicationV2Dto['application'],
  | 'updated'
  | 'created'
  | 'creator'
  | 'deletedAt'
  | 'deployedAt'
  | 'deployedCommitId'
  | 'folderId'
  | 'isDeployed'
  | 'isPublic'
  | 'isEditable'
  | 'userPermissions'
  | 'settings'
  | 'pageSummaryList'
> & {
  settings?: ExportedApplicationSettings;
  pages: { id: string; isDefault: boolean }[];
};
export type ExportedMultiPageApplication = Pick<
  FullApplicationV2Dto['application'],
  'id' | 'name' | 'organizationId' | 'configuration' | 'devEnvEnabled' | 'templateName'
> & {
  settings?: ExportedApplicationSettings;
};
export type ExportedPage = Omit<IPageV2Dto, 'updated'>;
export type ExportedPageWithApis = Omit<IPageV2Dto, 'updated' | 'apis'> & {
  apis?: ExportedApiV3Dto[];
};
type ExportedApplicationSettings = Omit<ApplicationSettings, 'profiles'>;

export type ExportedMultiPageApplicationDto = Omit<FullApplicationV2Dto, 'application' | 'page' | 'apis'> & {
  application: ExportedMultiPageApplication;
  apis?: ExportedApiV3Dto[];
  deployedCommitId?: string | null;
  pages?: ExportedPageWithApis[];
};

export function isFullApplicationV2Dto(value: unknown): value is FullApplicationV2Dto {
  return (
    isExportedApplicationDto(value) &&
    'updated' in value.application &&
    'created' in value.application &&
    'deletedAt' in value.application &&
    'folderId' in value.application &&
    'isDeployed' in value.application &&
    'isPublic' in value.application &&
    'isEditable' in value.application &&
    'userPermissions' in value.application
  );
}

export function isExportedApplicationDto(value: unknown): value is ExportedMultiPageApplicationDto {
  return Object.prototype.hasOwnProperty.call(value, 'application');
}

export interface IApplicationV2Dto {
  id: string;
  name: string;
  organizationId: string;
  pageSummaryList?: PageSummary[];
  isPublic: boolean;
  isDeployed: boolean;
  deployedCommitId?: string | null;
  deployedGitSha?: string | null;
  isEditable: boolean;
  canDelete?: boolean;
  userPermissions: string[];
  deletedAt: Date | null;
  updated: Date;
  created: Date;
  deployedAt?: Date | null;
  folderId: string | null;
  creator?: {
    id: string;
    name: string;
    email: string;
  };
  settings?: ApplicationSettings;
  signature?: ApplicationSignatureTreeSigned | null;
  configuration?: ApplicationConfiguration;
  repoConnection?: { created: Date; repository: RepositoryDto };
  currentBranch?: BranchDto;
  devEnvEnabled?: boolean;
  /** The name of the template used to create this application (e.g., 'shadcn-demo-app', 'blank-app'). Null for legacy apps. */
  templateName?: string | null;
  degradedMode?: DegradedMode;
  removedProfiles?: Record<string, Array<{ id: string; key: string; displayName: string }>>;
}

export interface IHomepageApplicationV2Dto extends IResourceMetadata {
  deployedAt?: Date | null;
  creator?: {
    id: string;
    name: string;
    email: string;
  };
}

export interface ApplicationV2GlobalDto {
  groups: DataTreeGroup[];
  user: DataTreeUser;
  versions: {
    current: CommitDto;
  };
}

export type PostApplicationCreateRequestBody = {
  name: string;
  // @format uuid
  organizationId: string;
  isPublic?: boolean;
  // @format uuid
  folderId?: string;
  settings?: Partial<ApplicationSettings>;
  configuration?: ApplicationConfiguration;
  // TODO: remove this once the UI is updated to use the RPC socket codepath
  signature?: ApplicationSignatureTreeSigned;
  ipCountry?: string; //ISO-3166-1 alpha-2 codes

  /** only used for DSL applications */
  initialPageDSL?: PageDSL;
  /** only used for React applications */
  createUsingReact?: boolean;
  /** which template to use when creating a React/code-mode application (e.g. 'shadcn-demo-app', 'blank-app') */
  templateName?: string;
  /** When provided, server generates an AI app name from this prompt */
  initialPrompt?: string;
};
export interface ApplicationPageClonePayload {
  applicationId: string;
  pageId: string;
  pageName: string;
  routePath: string;
  branch?: string;
  lastSuccessfulWrite: number;
  signingRequired: boolean;
}

export interface CreateApplicationPagePayload {
  // @format uuid, remove this field after FE uses new values
  pageId?: string;
  dsl?: PageDSL;
  // TODO (ahmet) make this required
  routePath?: string;
  testParams?: Record<string, string>;
  // TODO (ahmet) remove this field after FE uses new values
  signature?: ApplicationSignatureTreeSigned;
  lastSuccessfulWrite: number;
}

export type CreateApplicationPageSocketPayload = CreateApplicationPagePayload & {
  applicationId: string;
  signingRequired: boolean;
  branchName?: string;
  superblocksSupportUpdateEnabled?: boolean;
};

export type ModifyEntitiesPayload = {
  applicationId: string;
  appConfiguration: ApplicationConfigurationUpdate;
  // currently meant to store the current page DSL and changes
  pages: ModifyEntitiesPayloadPage[];
  apis: ModifyEntitiesPayloadApi[];
  renames?: ModifyEntityiesPayloadRename[];
  branch?: string;
  lastSuccessfulWrite: number;
  signingRequired: boolean;
};

type ModifyEntitiesPayloadPage = {
  pageId: string;
  dsl: PageDSL;
};

export type ModifyEntitiesPayloadApi = {
  apiId: string;
  pageId?: string;
  // matches closely to what the client would send
  dsl: {
    metadata?: Record<string, unknown>;
    blocks?: BaseBlock[];
    trigger: Record<string, unknown>;
    signature?: Signature;
  };
};

export type ModifyEntityiesPayloadRename = {
  newName: string;
  oldName: string;
  entityId: string;
};

export type ModifyEntitiesResponse = {
  updatedTime: number;
  updatedAPIs?: string[];
  signature?: ApplicationSignatureTreeSigned;
};

export enum ApplicationScope {
  APP = 'APP',
  PAGE = 'PAGE',
  GLOBAL = 'GLOBAL'
}

export type ApplicationHashChangeSource =
  | 'ai:reject'
  | 'ai:accept'
  | 'ai:generate:pre'
  | 'ai:generate:interim'
  | 'ai:generate:post'
  | 'rootUpdate'
  | 'multiRootUpdate'
  | 'rootReset'
  | 'fileChanged'
  | 'apiUpdate'
  | 'apiManualUpdate'
  | 'apiDelete'
  | 'apiManualDelete'
  | 'requestedFilesSync'
  | 'cli:sdk'
  | 'cli:git-sync'
  | 'initialSync';

/**
 * Returns true if the application's template uses the SDK API structure.
 * Legacy apps (templateName null/undefined) use YAML API.
 *
 * Must stay in sync with the `sdkApi` field in packages/demo-apps/config.json.
 */
const SDK_API_TEMPLATES = new Set(['app-fullstack']);

export function isSdkApiTemplate(templateName: string | null | undefined): boolean {
  if (!templateName) return false;
  return SDK_API_TEMPLATES.has(templateName);
}
