"""模块：通用 FormService 引擎

提供子服务器开箱即用的 ORM 表管理 API。
子服务器只需继承 FormServiceBase 并注册 TABLE_MAP 即可。

使用方式：
    # 子服务器 form_service.py
    from gvsdsdk.form_service import FormServiceBase
    from .models import MyModel1, MyModel2

    class FormService(FormServiceBase):
        TABLE_MAP = {
            'MyModel1': MyModel1,
            'MyModel2': MyModel2,
        }

    # 子服务器 views.py
    from gvsdsdk.form_service import FormServiceAPIView
    from .form_service import FormService
"""

import uuid
import datetime
import logging
from django.db.models import Q

logger = logging.getLogger(__name__)


def _pascalize_record(record, model=None):
    """将数据库记录的键名转为 PascalCase（使用 db_column）"""
    result = {}
    column_map = {}
    if model is not None:
        for f in model._meta.get_fields():
            if hasattr(f, 'db_column') and f.db_column:
                column_map[f.name] = f.db_column
                if hasattr(f, 'attname') and f.attname != f.name:
                    column_map[f.attname] = f.db_column
    for key, val in record.items():
        out_key = column_map.get(key, key)
        if isinstance(val, bytes) and len(val) == 16:
            try:
                val = str(uuid.UUID(bytes=val))
            except ValueError:
                pass
        elif isinstance(val, datetime.datetime):
            val = val.strftime('%Y-%m-%d %H:%M:%S')
        elif isinstance(val, datetime.date):
            val = val.isoformat()
        result[out_key] = val
    return result


class FormServiceBase:
    """通用 FormService 基类

    子类需要定义：
        TABLE_MAP: dict — 表名到 Django Model 的映射
        ENTITY_PICKER_MAP: dict — ForeignKey 到实体选择器的映射（可选）
        READONLY_TABLES_FOR_NORMAL: set — 普通用户只读的表（可选）
        ADMIN_ONLY_TABLES: set — 仅管理员可访问的表（可选）
    """

    TABLE_MAP = {}
    ENTITY_PICKER_MAP = {}
    READONLY_TABLES_FOR_NORMAL = set()
    ADMIN_ONLY_TABLES = set()

    def __init__(self, user=None):
        self.user = user

    def _build_field_map(self, model):
        pascal_map = {}
        for f in model._meta.get_fields():
            if hasattr(f, 'name'):
                pascal_map[f.name] = f
        return pascal_map

    def _resolve_field_name(self, key, pascal_map):
        if key in pascal_map:
            return pascal_map[key].name
        return None

    def _convert_value_for_field(self, field, val):
        if val is None or val == '':
            if getattr(field, 'null', False):
                return None
            return val
        field_type = field.__class__.__name__
        if field_type == 'UUIDField' and isinstance(val, str):
            try:
                return uuid.UUID(val).bytes
            except ValueError:
                return val
        if field_type == 'BinaryField' and isinstance(val, str):
            try:
                return uuid.UUID(val).bytes
            except ValueError:
                return val
        if field_type == 'ForeignKey' and isinstance(val, str):
            try:
                return uuid.UUID(val).bytes
            except ValueError:
                return val
        if field_type in ('SmallIntegerField', 'IntegerField', 'BigIntegerField',
                          'PositiveIntegerField', 'PositiveSmallIntegerField',
                          'FloatField', 'DecimalField') and isinstance(val, str):
            try:
                if field_type == 'FloatField':
                    return float(val)
                return int(val)
            except (ValueError, TypeError):
                return val
        if field_type == 'DateTimeField' and isinstance(val, str):
            try:
                return datetime.datetime.fromisoformat(val)
            except (ValueError, TypeError):
                return val
        if hasattr(field, 'max_length') and isinstance(val, str) and len(val) > field.max_length:
            val = val[:field.max_length]
        return val

    @staticmethod
    def _map_field_type(field):
        t = field.__class__.__name__
        mapping = {
            'UUIDField': 'uuid',
            'BinaryField': 'uuid',
            'FixedBinaryField': 'uuid',
            'CharField': 'text',
            'TextField': 'textarea',
            'JSONField': 'textarea',
            'SmallIntegerField': 'number',
            'IntegerField': 'number',
            'BigIntegerField': 'number',
            'PositiveIntegerField': 'number',
            'PositiveSmallIntegerField': 'number',
            'FloatField': 'number',
            'DecimalField': 'number',
            'BooleanField': 'boolean',
            'NullBooleanField': 'boolean',
            'DateTimeField': 'datetime',
            'DateField': 'date',
            'TimeField': 'time',
            'ForeignKey': 'uuid',
            'OneToOneField': 'uuid',
            'ManyToManyField': 'text',
            'ImageField': 'text',
            'FileField': 'text',
            'SlugField': 'text',
            'EmailField': 'text',
            'URLField': 'text',
            'AutoField': 'number',
            'BigAutoField': 'number',
        }
        if t == 'CharField' and hasattr(field, 'choices') and field.choices:
            return 'select'
        return mapping.get(t, 'text')

    def get_tables(self):
        tables = []
        for name, model in self.TABLE_MAP.items():
            try:
                count = model.objects.count()
            except Exception:
                count = -1
            tables.append({
                'name': name,
                'title': name,
                'count': count,
                'type': 'dynamic',
                'source': 'model',
            })
        return tables

    def get_columns(self, table_name):
        model = self.TABLE_MAP.get(table_name)
        if not model:
            return []
        try:
            columns = []
            for f in model._meta.get_fields():
                if not hasattr(f, 'name') or f.name in ('id',):
                    continue
                if f.auto_created and not f.concrete:
                    continue
                if f.is_relation and not f.concrete:
                    continue
                is_pk = (f.name == model._meta.pk.name) if model._meta.pk else False
                pascal_name = f.name
                field_type = self._map_field_type(f)
                col = {
                    'field': pascal_name,
                    'title': pascal_name,
                    'type': field_type,
                    'editable': getattr(f, 'editable', True) and not is_pk,
                    'sortable': True,
                    'filterable': True,
                    'primaryKey': is_pk,
                    'hideInForm': is_pk,
                }
                if field_type == 'select' and hasattr(f, 'choices') and f.choices:
                    col['options'] = [{'value': k, 'label': str(v)} for k, v in f.choices]
                if field_type == 'number':
                    col['min'] = None
                    col['max'] = None
                    for v in getattr(f, 'validators', []):
                        vname = v.__class__.__name__
                        if vname == 'MinValueValidator':
                            col['min'] = v.limit_value
                        elif vname == 'MaxValueValidator':
                            col['max'] = v.limit_value
                    if col['min'] is None and col['max'] is None:
                        col.pop('min', None)
                        col.pop('max', None)
                if field_type == 'text' and hasattr(f, 'max_length') and f.max_length:
                    col['maxLength'] = f.max_length
                if getattr(f, 'null', False):
                    col['required'] = False
                # ForeignKey 字段：检测关联模型，自动使用实体选择器
                if f.__class__.__name__ == 'ForeignKey':
                    related_model = getattr(f, 'related_model', None)
                    if related_model:
                        related_name = related_model.__name__
                        ep_config = self.ENTITY_PICKER_MAP.get(related_name)
                        if ep_config:
                            col['type'] = 'entityPicker'
                            col['entityPicker'] = ep_config
                columns.append(col)
            return columns
        except Exception:
            return []

    def get_table(self, table_name, params):
        model = self.TABLE_MAP.get(table_name)
        if not model:
            return {'records': [], 'total': 0}
        qs = model.objects.all()
        keyword = params.get('keyword', '')
        if keyword:
            or_conditions = {}
            for f in model._meta.get_fields():
                if hasattr(f, 'name') and f.__class__.__name__ in ('CharField', 'TextField'):
                    or_conditions[f'{f.name}__icontains'] = keyword
            if or_conditions:
                q = Q()
                for k, v in or_conditions.items():
                    q |= Q(**{k: v})
                qs = qs.filter(q)
        try:
            page = int(params.get('PageIndex', 1))
        except (ValueError, TypeError):
            page = 1
        try:
            page_size = int(params.get('PageSize', 20))
        except (ValueError, TypeError):
            page_size = 20
        total = qs.count()
        start = (page - 1) * page_size
        records = [_pascalize_record(r, model) for r in qs[start:start + page_size].values()]
        return {'records': records, 'total': total, 'page': page, 'PageSize': page_size}

    def add_record(self, data):
        table_name = data.get('Table', '')
        model = self.TABLE_MAP.get(table_name)
        if not model:
            return None
        pascal_map = self._build_field_map(model)
        create_data = {}
        for key, val in data.items():
            if key in ('table', 'action', 'Table', 'Action', 'pk', 'PK', 'uuid', 'UUID'):
                continue
            resolved = self._resolve_field_name(key, pascal_map)
            if resolved is None:
                continue
            field = pascal_map.get(resolved) or pascal_map.get(key)
            if field is None:
                continue
            if isinstance(val, list) and len(val) == 1:
                val = val[0]
            val = self._convert_value_for_field(field, val)
            is_fk = field.__class__.__name__ == 'ForeignKey'
            attr_name = field.attname if is_fk else resolved
            create_data[attr_name] = val
        pk_field_name = model._meta.pk.name if model._meta.pk else None
        if pk_field_name and pk_field_name not in create_data:
            pk_field = model._meta.pk
            if pk_field.__class__.__name__ in ('UUIDField', 'BinaryField', 'FixedBinaryField'):
                create_data[pk_field_name] = uuid.uuid4().bytes
        obj = model.objects.create(**create_data)
        return {'PK': str(obj.pk) if obj.pk else None}

    def modify_record(self, data):
        table_name = data.get('Table', '')
        model = self.TABLE_MAP.get(table_name)
        if not model:
            return None
        pk = data.get('PK')
        if not pk:
            return None
        pk_field_name = model._meta.pk.name
        pk_field = model._meta.pk
        pk_value = self._convert_value_for_field(pk_field, pk)
        try:
            obj = model.objects.get(**{pk_field_name: pk_value})
        except model.DoesNotExist:
            return None
        pascal_map = self._build_field_map(model)
        update_fields = []
        for key, val in data.items():
            if key in ('table', 'action', 'pk', 'uuid', 'Table', 'Action', 'PK', 'UUID'):
                continue
            resolved = self._resolve_field_name(key, pascal_map)
            if resolved is None:
                continue
            if resolved == model._meta.pk.name:
                continue
            field = pascal_map.get(resolved) or pascal_map.get(key)
            if field is None:
                continue
            if isinstance(val, list) and len(val) == 1:
                val = val[0]
            val = self._convert_value_for_field(field, val)
            is_fk = field.__class__.__name__ == 'ForeignKey'
            attr_name = field.attname if is_fk else resolved
            setattr(obj, attr_name, val)
            update_fields.append(attr_name)
        if update_fields:
            obj.save(update_fields=update_fields)
        return {'PK': str(obj.pk) if obj.pk else None}

    def modify_item(self, data):
        return self.modify_record(data)

    def delete_record(self, data):
        table_name = data.get('Table', '')
        model = self.TABLE_MAP.get(table_name)
        if not model:
            return False
        pk = data.get('PK')
        if not pk:
            return False
        pk_field_name = model._meta.pk.name
        pk_field = model._meta.pk
        pk_value = self._convert_value_for_field(pk_field, pk)
        try:
            model.objects.get(**{pk_field_name: pk_value}).delete()
            return True
        except model.DoesNotExist:
            return False

    def code_display(self, data):
        table_name = data.get('Table', '')
        code_field = data.get('CodeField', 'PermCode')
        name_field = data.get('NameField', 'PermName')
        model = self.TABLE_MAP.get(table_name)
        if not model:
            return []
        fields = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')}
        if code_field not in fields or name_field not in fields:
            return []
        qs = model.objects.all()
        keyword = data.get('Keyword', '')
        if keyword:
            filter_kw = {f'{code_field}__icontains': keyword}
            qs = qs.filter(**filter_kw)
        results = []
        for obj in qs[:50]:
            results.append({
                'code': getattr(obj, code_field, ''),
                'name': getattr(obj, name_field, ''),
            })
        return results

    def handle_request(self, action, data):
        """统一请求入口

        Args:
            action: 操作类型 (tables/columns/list/add/modify/delete/code_display)
            data: 请求数据

        Returns:
            dict 响应数据
        """
        if action == 'tables':
            return {'code': 0, 'msg': 'success', 'data': self.get_tables()}
        elif action == 'columns':
            table_name = data.get('Table', '')
            if table_name not in self.TABLE_MAP:
                return {'code': 1, 'msg': f'表 {table_name} 不存在'}
            return {'code': 0, 'msg': 'success', 'data': self.get_columns(table_name)}
        elif action == 'list':
            table_name = data.get('Table', '')
            if table_name not in self.TABLE_MAP:
                return {'code': 1, 'msg': f'表 {table_name} 不存在'}
            return {'code': 0, 'msg': 'success', 'data': self.get_table(table_name, data)}
        elif action == 'add':
            result = self.add_record(data)
            if result is None:
                return {'code': 1, 'msg': '添加失败'}
            return {'code': 0, 'msg': 'success', 'data': result}
        elif action == 'modify':
            result = self.modify_record(data)
            if result is None:
                return {'code': 1, 'msg': '修改失败'}
            return {'code': 0, 'msg': 'success', 'data': result}
        elif action == 'delete':
            result = self.delete_record(data)
            if not result:
                return {'code': 1, 'msg': '删除失败'}
            return {'code': 0, 'msg': 'success'}
        elif action == 'code_display':
            result = self.code_display(data)
            return {'code': 0, 'msg': 'success', 'data': result}
        else:
            return {'code': 1, 'msg': f'未知操作: {action}'}
