import os
import tempfile
import uuid
import re
import requests
import datetime
import subprocess
import threading
import time
import base64
import hashlib
import mimetypes
from collections import defaultdict
from tastypie.resources import ModelResource
from .models import *
from tastypie.api import Api
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
from django.http import HttpResponse, JsonResponse, FileResponse
from django.db.models import Q, Count
from django.db import transaction
from django.core.cache import cache
from django.views import View
from django.core.exceptions import *
from tastypie import fields
from tastypie.authorization import Authorization
from tastypie.serializers import Serializer
from django.shortcuts import get_object_or_404, redirect
from django.utils import timezone
from django.conf import settings
from random import randint
from .aliyun import *
from notify import send_sms_code
from .ip2region import *
from .miniprogram import mplink
from .sendmsg import admin_broadcast
from .article import gen_article_css
from .aichat import AIChatService

MANAGE_PY = os.path.join(settings.BASE_DIR, 'manage.py')

v1_api = Api(api_name='v1')

def get_ip(request):
    if False:
        # The current deployment does have proper X-Real-IP
        realip = request.data.get("realip")
        if realip:
            return realip

    headers = ['X-Real-IP', 'X-Forwarded-For']
    for h in headers:
        rip = request.headers.get(h)
        if rip:
            return rip
    return request.META.get('REMOTE_ADDR', "0.0.0.0")

class ForeignKey(fields.ForeignKey):
    def build_related_resource(self, value, request=None, related_obj=None, related_name=None):
        if isinstance(value, str) and value.isnumeric():
            value = int(value)
        if isinstance(value, int):
            value = { 'pk': value }
        return super().build_related_resource(value, request, related_obj, related_name)

class AuthenticationError(Exception):
    pass

class BaseSerializer(Serializer):
    def format_datetime(self, data):
        return data.timestamp()

def get_token(request):
    auth_header = request.headers.get("Authorization", "")
    if not auth_header:
        token = request.GET.get("token")
        if token:
            return token
    fs = auth_header.split(' ', maxsplit=1)
    if len(fs) != 2:
        return None
    return fs[1]

def do_authenticate(request):
    token = get_token(request)
    request.admin = None
    request.tenant = None
    if token:
        t = AuthToken.objects.filter(token=token).first()
        if t and t.validate():
            request.admin = t.admin
            request.tenant = t.tenant
        return True
    return False

class BaseResource(ModelResource):
    def build_schema(self):
        ret = super().build_schema()
        return self.patch_schema(ret)

    def patch_schema(self, schema):
        return schema

    def get_schema(self, request, **kwargs):
        if not self.auth(request):
            return http401()
        return super().get_schema(request, **kwargs)

    def auth(self, request):
        ac = getattr(self.Meta, 'auth_role', 'admin')
        do_authenticate(request)
        if ac is None:
            return True
        if ac == 'tenant':
            ok = request.admin or request.tenant
        elif ac == 'anonymous':
            ok = True
        else:
            ok = request.admin
        return ok

    def dispatch(self, request_type, request, **kwargs):
        request.request_type = request_type
        if not self.auth(request):
            return http401()
        return super().dispatch(request_type, request, **kwargs)

    def filter_by_q(self, request, qs):
        q = request.GET.get('q')
        if q:
            qf = self.Meta.default_query_field
            if isinstance(qf, str):
                qf = [qf]
            qobj = None
            for f in qf:
                if f.startswith("="):
                    kwargs = {'%s' % f[1:]: q}
                else:
                    kwargs = {'%s__icontains' % f: q}
                if not qobj:
                    qobj = Q(**kwargs)
                else:
                    qobj = qobj | Q(**kwargs)
            qs = qs.filter(qobj)
        return qs

    def get_object_list(self, request):
        qs = self.Meta.queryset
        if not request.admin and self.Meta.auth_role == 'tenant':
            if hasattr(self.Meta, 'tenant_query_key'):
                k = self.Meta.tenant_query_key
            else:
                k = 'tenant'
            kwargs = { k: request.tenant }
            qs = qs.filter(**kwargs)
        return self.filter_by_q(request, qs)

    def annotate_field(self, schema, field_name, verbose_name, type_="string"):
        schema['fields'][field_name] = {
            'verbose_name': verbose_name,
            "blank": True,
            "default": "",
            "help_text": "",
            "nullable": True,
            "primary_key": False,
            "readonly": True,
            "type": type_,
            "unique": False,
        }
        return schema

    def patch_product_name_schema(self, schema):
        self.annotate_field(schema, 'product__name', '产品')
        return schema

class BaseAuthorization(Authorization):
    pass

class GlobalConfigResource(BaseResource):
    class Meta:
        queryset = GlobalConfig.objects.all()
        resource_name = 'config'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        default_query_field = ['name']
        auth_role = 'admin'

class ArticleResource(BaseResource):
    class Meta:
        queryset = Article.objects.all().order_by("-pk")
        resource_name = 'article'
        authorization = BaseAuthorization()

        auth_role = 'tenant'
        default_query_field = 'title'

    def hydrate(self, bundle):
        bundle.obj.tenant = bundle.request.tenant
        return super().hydrate(bundle)

    def dehydrate(self, bundle):
        if bundle.request.request_type == 'list':
            del bundle.data['body']
        return bundle

class AssetFileResource(BaseResource):
    class Meta:
        queryset = AssetFile.objects.all().order_by('-pk');
        resource_name = 'asset-file'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()
        default_query_field = 'filename'

        auth_role = 'tenant'

    def hydrate(self, bundle):
        bundle.obj.tenant = bundle.request.tenant
        return super().hydrate(bundle)

    def dehydrate(self, bundle):
        obj = bundle.obj
        url = "/api/v1/asset/?id=%d" % obj.pk
        bundle.data['url'] = url
        bundle.data['tenant__id'] = obj.tenant.pk if obj.tenant else None
        bundle.data['display_field'] = 'filename'
        return bundle

class TenantResource(BaseResource):
    service_qr_file = ForeignKey(AssetFileResource, 'service_qr_file', null=True, blank=True, full=True)
    class Meta:
        queryset = Tenant.objects.all().order_by('-pk')
        resource_name = 'tenant'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        default_query_field = ['name']
        auth_role = 'admin'

    def patch_schema(self, schema):
        schema['fields']['service_qr_file']['verbose_name'] = '人工客服二维码文件'
        schema['fields']['service_qr_file']['display_field'] = 'filename'
        return schema

class ProductResource(BaseResource):
    article = ForeignKey(ArticleResource, 'article', null=True, blank=True, full=True)
    template_asset = ForeignKey(AssetFileResource, 'template_asset', null=True, blank=True, full=True)

    class Meta:
        queryset = Product.objects.all().order_by('-pk')
        resource_name = 'product'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()
        default_query_field = 'name'
        always_return_data = True

        auth_role = 'tenant'

    def hydrate(self, bundle):
        bundle.obj.tenant = bundle.request.tenant
        return super().hydrate(bundle)

    def dehydrate(self, bundle):
        obj = bundle.obj
        if obj.article:
            bundle.data['article__id'] = obj.article.pk
            bundle.data['article__title'] = obj.article.title
        if obj.template_asset:
            bundle.data['template_asset__filename'] = obj.template_asset.filename
        return bundle

    def patch_schema(self, schema):
        schema['fields']['description']['longtext'] = True
        self.annotate_field(schema, 'article__title', '页面')
        self.annotate_field(schema, 'template_asset__filename', '模板')
        schema['fields']['template_asset']['verbose_name'] = '模板文件'
        schema['fields']['template_asset']['display_field'] = 'filename'
        schema['fields']['article']['verbose_name'] = '产品页面'
        schema['fields']['article']['display_field'] = 'title'
        return schema

class ProductPropertyResource(BaseResource):
    file = ForeignKey(AssetFileResource, 'file', null=True, blank=True, full=True)

    class Meta:
        queryset = ProductProperty.objects.all().order_by('-pk')
        resource_name = 'product-property'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()
        default_query_field = 'product__id'
        tenant_query_key = 'product__tenant'
        always_return_data = True

        auth_role = 'tenant'

    def patch_schema(self, schema):
        schema['fields']['richtext']['richtext'] = True
        schema['fields']['memo']['multiline'] = True
        schema['fields']['file']['verbose_name'] = '文件'
        schema['fields']['file']['display_field'] = 'filename'
        return schema

    def get_object_list(self, request):
        r = super().get_object_list(request)
        if 'product_id' in request.GET:
            product = get_object_or_404(Product, tenant=request.tenant, pk=request.GET.get('product_id'))
            return r.filter(product=product)
        else:
            return r

    def hydrate(self, bundle):
        tenant = bundle.request.tenant
        bundle.obj.product = get_object_or_404(Product, pk=bundle.data.get('product_id'), tenant=tenant)
        bundle.obj.tenant = tenant
        return super().hydrate(bundle)

class CodeBatchResource(BaseResource):
    tenant = ForeignKey(TenantResource, 'tenant', null=True, blank=True)
    class Meta:
        queryset = CodeBatch.objects.all().order_by('-pk')
        resource_name = 'code-batch'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        auth_role = 'tenant'
        always_return_data = True
        default_query_field = ['name']

    def dehydrate(self, bundle):
        obj = bundle.obj
        bundle.data['code__count'] = obj.codes.count()
        if obj.tenant:
            bundle.data['tenant__username'] = obj.tenant.username
        return bundle

    def hydrate(self, bundle):
        bundle.obj.tenant = bundle.request.tenant
        return super().hydrate(bundle)

    def patch_schema(self, schema):
        schema['fields']['description']['longtext'] = True
        self.annotate_field(schema, 'code__count', '序列码数量')
        self.annotate_field(schema, 'tenant__username', '租户')
        return self.patch_product_name_schema(schema)

class CodeResource(BaseResource):
    batch = ForeignKey(CodeBatchResource, 'batch')

    class Meta:
        queryset = SerialCode.objects.all().order_by('-seq_num')
        resource_name = 'code'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        auth_role = 'tenant'

        default_query_field = ['=code', '=seq_num']
        filtering = {"code": ("exact", "in",), }

    def dehydrate(self, bundle):
        obj = bundle.obj
        if obj.product:
            bundle.data['product__name'] = obj.product.name
        bundle.data['batch__name'] = obj.batch.name
        bundle.data['qr_url'] = obj.get_qr_url()
        return bundle

    def patch_schema(self, schema):
        self.annotate_field(schema, 'batch__name', '批次名', 'str')
        return self.patch_product_name_schema(schema)

class CodeBatchOpRecordResource(BaseResource):

    class Meta:
        queryset = SerialCodeBatchOpRecord.objects.all().order_by('-pk')
        resource_name = 'code-batch-op-record'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        auth_role = 'tenant'

        default_query_field = ['op_name', 'product_name', 'product_page_name', 'seq_num_begin', 'seq_num_end', 'code_samples']

class LogResource(BaseResource):
    class Meta:
        queryset = SystemLog.objects.all().order_by('-datetime')
        resource_name = 'log'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        auth_role = 'admin'
        default_query_field = 'log'

class ScanDataResource(BaseResource):
    class Meta:
        queryset = ScanData.objects.all().order_by('-pk')
        resource_name = 'scan-data'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        auth_role = 'tenant'

        default_query_field = ['id', 'consumer__username',
                               'product__name', 'ip',
                               'image',
                               'location', 'message',
                               'code', 'phone_model',
                               'labels'
                               ]

    def dehydrate(self, bundle):
        obj = bundle.obj
        bundle.data['product__name'] = obj.product.name if obj.product else None
        return bundle

    def patch_schema(self, schema):
        schema = self.patch_product_name_schema(schema)
        schema['fields']['message']['multiline'] = True
        schema['fields']['client_log']['multiline'] = True

        return schema

    def get_object_list(self, request):
        qs = super().get_object_list(request)
        if 'include_labels' in request.GET:
            labels = request.GET.get('include_labels').split(',')
            for l in labels:
                qs = qs.filter(labels__contains=l)
        if 'exclude_labels' in request.GET:
            labels = request.GET.get('exclude_labels').split(',')
            for l in labels:
                qs = qs.exclude(labels__contains=l)
        return qs

class MessageResource(BaseResource):
    class Meta:
        queryset = UserMessage.objects.all().order_by('-pk');
        resource_name = 'message'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()
        default_query_field = 'subject'

        auth_role = 'tenant'

    def get_object_list(self, request):
        qs = self.Meta.queryset
        assert request.admin or request.tenant
        qs = qs.filter(admin=request.admin, tenant=request.tenant)
        return self.filter_by_q(request, qs)

class JobResource(BaseResource):
    class Meta:
        queryset = Job.objects.all().order_by('-pk');
        resource_name = 'job'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        auth_role = 'tenant'

class MiniProgramResource(BaseResource):
    class Meta:
        queryset = MiniProgram.objects.all().order_by('-pk');
        resource_name = 'mini-program'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        auth_role = 'admin'

class CameraRuleResource(BaseResource):
    class Meta:
        queryset = CameraRule.objects.all().order_by('-pk');
        resource_name = 'camera-rule'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()
        default_query_field = 'name'

        auth_role = 'admin'

    def patch_schema(self, schema):
        schema['fields']['notes']['longtext'] = True
        return schema

class ABTestResource(BaseResource):
    class Meta:
        queryset = ABTest.objects.all().order_by('-pk')
        resource_name = 'abtest'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        auth_role = 'admin'

class ABTestSampleResource(BaseResource):
    class Meta:
        queryset = ABTestSample.objects.all().order_by('-pk')
        resource_name = 'abtestsample'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        auth_role = 'admin'

class DataLabelResource(BaseResource):
    class Meta:
        queryset = DataLabel.objects.all().order_by('-pk')
        resource_name = 'datalabel'
        authorization = BaseAuthorization()
        serializer = BaseSerializer()

        auth_role = 'admin'

for cls in BaseResource.__subclasses__():
    v1_api.register(cls())

def http302(loc):
    r = HttpResponse("")
    r['Location'] = loc
    r.status_code = 302
    return r

def http401():
    r = HttpResponse("Unauthorized")
    r.status_code = 401
    return r

def http404(resp="Requested resource is not found"):
    r = HttpResponse(resp)
    r.status_code = 404
    return r

def http400(msg="Invalid request"):
    r = HttpResponse(msg)
    r.status_code = 400
    return r

def http405():
    r = HttpResponse("Unsupported method")
    r.status_code = 405
    return r

def code_to_qr_std_key(code):
    code = str(code)
    pref = code[:2]
    return f'v5/{pref}/{code}.jpg'

class BaseView(View):
    name = ''
    path_prefix = 'v1/'

    auth_check = None

    @classmethod
    def get_path(self):
        return self.path_prefix + self.name + '/'

    def get_sha1(self, data):
        if isinstance(data, str):
            data = data.encode()
        return hashlib.sha1(data).hexdigest()

    def save_file(self, file_upload):
        tf = tempfile.NamedTemporaryFile(dir="/var/tmp/", delete=False, suffix=file_upload.name)
        for c in file_upload.chunks():
            tf.write(c)
        tf.flush()
        return tf.name

    def dispatch(self, request, *args, **kwargs):
        if request.content_type == 'application/json':
            try:
                request.data = json.loads(request.read())
            except:
                request.data = {}
        else:
            request.data = request.POST
        do_authenticate(request)
        if self.auth_check == 'admin' and not request.admin:
            return http401()
        if self.auth_check == 'tenant' and not request.admin and not request.tenant:
            return http401()
        return super().dispatch(request, *args, **kwargs)

    def get_qr_std(self, code):
        return oss_get(code_to_qr_std_key(code), bucket=settings.OSS_QRS_BUCKET, endpoint=settings.OSS_QRS_ENDPOINT)

class UserInfoView(BaseView):
    name = 'userinfo'

    def get(self, request):
        ret = {
            'is_admin': False,
            'username': None,
            'email': None,
        }
        if request.admin:
            ret['is_admin'] = True
            ret['username'] = request.admin.user.username
            ret['email'] = request.admin.user.email
        else:
            if not request.tenant:
                return http401()
            ret['username'] = request.tenant.username
        return JsonResponse(ret)

class LoginView(BaseView):
    name = 'login'
    def post(self, request):
        username = request.data.get("username")
        password = request.data.get("password")
        if not username or not password:
            return http401()
        user = authenticate(username=username, password=password)
        if user:
            ai, created = AdminInfo.objects.get_or_create(user=user)
            log("admin login:", user.username)
            token = AuthToken.objects.create(token=str(uuid.uuid4()), admin=ai)
            token.update_expire()
            token_for_estor = AuthToken.objects.filter(admin=ai, expire=None).first()
            if not token_for_estor:
                token_for_estor = AuthToken.objects.create(token=str(uuid.uuid4()), admin=ai)
            return JsonResponse({"token": token.token, 'role': 'admin', 'id': user.pk, 'token_for_estor': token_for_estor.token})
        else:
            tenant = Tenant.authenticate(username, password)
            if tenant:
                log("tenant login:", tenant.username)
                token = AuthToken.objects.create(token=str(uuid.uuid4()), tenant=tenant)
                token.update_expire()
                return JsonResponse({"token": token.token, 'role': 'tenant', 'id': tenant.pk})
        return http401()

def get_image_from_data_url(data_url):
    pref = "data:image/jpeg;base64,"
    if data_url and data_url.startswith(pref):
        return base64.b64decode(data_url[len(pref):]), "image.jpg"
    pref = "data:image/png;base64,"
    if data_url and data_url.startswith(pref):
        return base64.b64decode(data_url[len(pref):]), "image.png"

def get_images(request):
    f = request.data.get("image_data_urls")
    if f:
        return [get_image_from_data_url(d) for d in f]
    return [get_image(request)]

def get_image(request):
    f = request.FILES.get('photo') or request.FILES.get('file')
    if f:
        return f.read(), f.name
    f = request.data.get("image_data_url")
    if f:
        return get_image_from_data_url(f)
    return None, None

def get_code_from_url(url):
    code = None
    patterns = [
            '[?&]code=([0-9a-zA-Z]+)',
            '[?&]id=([0-9a-zA-Z]+)',
            '[?&]c=([0-9a-zA-Z]+)',
            'xy.ltd/v/([0-9a-zA-Z]+)'
    ]
    for p in patterns:
        m = re.search(p, url)
        if m:
            return m.group(1)

class QrVerifyView(BaseView):
    name = 'qr-verify'

    def do_v5_qr_verify(self, imgs, messages):
        files = {}
        for i, img in enumerate(imgs):
            files[f'frame_{i}_{img[1]}'] = img[0]
        resp = requests.post(
            "https://themblem.com/api/v5/qr_verify",
            files=files,
        )
        rd = resp.json()
        messages.append(f"v5 qr_verify response: {rd}")
        ok = rd.get("result", {}).get("predicted_class", 0) == 1
        if not ok:
            raise Exception("v5 qr verify failed")

    def post(self, request):
        image_name = ''
        tenant = None
        sd = ScanData(succeeded=False)
        sd.message = ''
        sd.phone_model = request.data.get("phonemodel")
        sd.client_log = request.data.get("log")
        messages = []
        try:
            qrcode = request.data.get("qrcode")
            if not qrcode:
                return http400("qrcode is missing")
            code = get_code_from_url(qrcode)
            if not code:
                raise Exception("Unknown URL pattern: %s" % qrcode)
            sd.code = code

            sd.ip = get_ip(request)
            sd.consumer = get_consumer(request.data.get('username'), sd.ip)
            sd.location = ip_to_region(sd.ip)

            sc = SerialCode.objects.filter(code=code).first()
            if not sc or not sc.product:
                raise Exception('No matching product found with code: %s' % code)
            product = sc.product
            if not sc.batch.is_active or not sc.is_active:
                raise Exception("Inactive code")
            sd.product = product
            tenant = product.tenant
            sd.tenant = tenant
            sd.batch = sc.batch
            imgs = get_images(request)
            if not imgs:
                return http400("image is missing")
            img_data, filename = imgs[0]
            image_name = 'scandata_v2/' + str(uuid.uuid4()) + '-' + filename
            sd.image = image_name
            if settings.ENV != "DEBUG" or os.environ.get("EMBLEM_ENABLE_OSS"):
                t = threading.Thread(target=oss_put, args=(image_name, img_data))
                t.run()
            if sc.batch.feature_comparison_threshold > 0.01:
                self.do_v5_qr_verify(imgs, messages)
            else:
                messages.append("skip v5 qr verify")
            sd.succeeded = True
            article_id = None
            if product.article:
                article_id = product.article.pk
            count(product.tenant,
                "qr-verify-ok", {
                'image': image_name,
                'product': product.pk,
                })
            resp = {
                'id': product.pk,
                'link': article_id,
                'serial_code': code,
            }
        except Exception as e:
            messages.append(str(e))
            sd.save()
            count(tenant, "qr-verify-fail", {
                'image': image_name,
                'reason': str(e),
                })
            resp = {
                'error': 'qr verify failed: %s' % str(e),
                'ref': sd.pk,
            }
        finally:
            sd.message = "\n".join(messages) + "\n"
            sd.save()
            if sd.code and not settings.DEBUG:
                cmd = [MANAGE_PY, 'qrrepeatalert', code]
                subprocess.Popen(cmd)
        return JsonResponse(resp)

class CheckAutoTorchView(BaseView):
    name = 'check-auto-torch'
    def get(self, request):
        qrcode = request.GET.get('qrcode')
        if not qrcode:
            return http404()

        code = get_code_from_url(qrcode)
        if not code:
            return http404("cannot find serial code from qrcode")
        c = get_object_or_404(SerialCode, code=code)
        b = c.batch
        return JsonResponse({
            "code": code,
            "enable_auto_torch": b.enable_auto_torch,
            "camera_sensitivity": b.camera_sensitivity,
            })

class RequestPasswordResetView(BaseView):
    name = 'request-password-reset'
    def post(self, request):
        tenant = get_object_or_404(Tenant, mobile=request.data.get('mobile'))
        log("tenant request password reset:", tenant.username)
        code = str(randint(100000, 999999))
        tenant.password_reset_code = code
        expire = timezone.now() + datetime.timedelta(minutes=5)
        tenant.password_reset_code_expire = expire
        tenant.save()
        send_sms_code(tenant.mobile, code)
        return JsonResponse({})

class RequestPasswordResetView(BaseView):
    name = 'reset-password'
    def post(self, request):
        now = timezone.now()
        tenant = get_object_or_404(Tenant,
                mobile=request.data.get('mobile'),
                password_reset_code=request.data.get('verify_code'),
                password_reset_code_expire__gt=now)
        if tenant:
            log("tenant reset password:", tenant.username)
            tenant.password = data.get('password')
            tenant.save()
            return JsonResponse({})
        else:
            return http401()

class ExportBatchView(BaseView):
    name = 'export-batch'
    auth_check = 'admin'

    def post(self, request):
        pk = request.data.get('id')
        pattern = request.data.get('pattern')
        batch = get_object_or_404(CodeBatch, pk=int(pk))
        tf = tempfile.NamedTemporaryFile(prefix='export-batch-%s-' % pk, suffix='.txt', mode='w', delete=False)
        job = Job.objects.create(name='export-batch.%s' % pk, admin=request.admin, output='file://' + tf.name)
        cmd = [MANAGE_PY, 'exportbatch', '-b', str(pk), '-o', tf.name, '-j', str(job.pk)]
        if pattern:
            cmd += ['-P', pattern]
        subprocess.Popen(cmd)

        return JsonResponse({
            'job': job.pk,
        })

class JobOutputView(BaseView):
    name = 'job-output'
    auth_check = 'admin'

    def get(self, request):
        pk = request.GET.get('id')
        job = get_object_or_404(Job, pk=pk, status="done")
        if job.output.startswith("file://"):
            fn = job.output[len("file://"):]
            resp = FileResponse(open(fn, 'rb'), as_attachment=True)
        else:
            resp = HttpResponse(job.output)
        return resp

class GenCodeView(BaseView):
    name = 'gen-code'
    auth_check = 'admin'

    def post(self, request):
        batch_id = request.data.get('batch_id')
        batch = get_object_or_404(CodeBatch, pk=int(batch_id))
        max_batch = 10000
        n = int(request.data.get("num", max_batch))
        if n < 0 or n > max_batch:
            n = max_batch
        batch.gen_code(n)
        return JsonResponse({'created': n})

class ImportCodeView(BaseView):
    name = 'import-code'
    auth_check = 'admin'

    def post(self, request):
        import_file = request.FILES.get('file')
        if not import_file:
            return http400('File not found in request')
        batch_id = request.POST.get('batch')
        batch = get_object_or_404(CodeBatch, pk=batch_id)

        fn = self.save_file(import_file)
        job = Job.objects.create(name='import-code.%d' % batch.pk)
        cmd = [MANAGE_PY, 'importcode', '-f', fn, '-b', str(batch.pk), '-j', str(job.pk)]
        subprocess.Popen(cmd)

        return JsonResponse({'job': job.pk})


class BindBatchView(BaseView):
    name = 'bind-batch'
    auth_check = 'admin'

    def post(self, request):
        batch_id = request.data.get('batch_id')
        batch = get_object_or_404(CodeBatch, pk=int(batch_id))
        tenant = request.data.get('tenant_username')
        if tenant:
            tenant = get_object_or_404(Tenant, username=tenant)
            batch.tenant = tenant
            batch.save()
        else:
            batch.tenant = None
            batch.save()
        job = Job.objects.create(name='bind-batch.%d' % batch.pk)
        cmd = [MANAGE_PY, 'bindbatch', '-b', str(batch.pk), '-j', str(job.pk)]
        subprocess.Popen(cmd)

        return JsonResponse({'job': job.pk})

def get_article_body(t, raw):
    content = t.body
    if raw:
        return content
    if '<html>' in content:
        return content
    ret = """
    <html>
    <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title></title>
    <link rel="stylesheet" href="/api/v1/article.css?id={pk}">
    <body>
    <div class="article">
      {content}
    </div>
    </body>
    </html>
    """.format(content=content, pk=t.pk)
    return ret

class ArticleView(BaseView):
    name = 'article'
    path_prefix = ''

    def get(self, request):
        pk = request.GET.get('id')
        if not pk or not pk.isnumeric():
            return http404()

        t = get_object_or_404(Article, pk=int(pk))
        if t.url:
            return redirect(t.url)
        body = get_article_body(t, request.GET.get("raw"))
        return HttpResponse(body, content_type='text/html; charset=UTF-8')

class ProductInfoView(BaseView):
    name = 'product-info'
    path_prefix = ''

    @classmethod
    def get_urls(self):
        from django.urls import re_path
        return [
                re_path(r'product-info/(?P<code>\w+)/(?P<path>.*)$', ProductInfoView.as_view()),
        ]

    def get(self, request, code, path):
        sc = get_object_or_404(SerialCode, code=code)
        if not sc.product:
            return http404()
        if path.split('/')[:2] == ["api", "properties"]:
            return self.handle_properties(sc)
        article = sc.product.article
        if article:
            if article.url:
                return redirect(article.url)
            body = get_article_body(article, request.GET.get("raw"))
            return HttpResponse(body, content_type='text/html; charset=UTF-8')
        if sc.product.template_asset:
            return self.handle_template(sc, path)
        return http404()

    def handle_properties(self, sc):
        ret = {}
        for p in sc.product.properties.all():
            ret[p.name] = {
                "text": p.text,
                "richtext": p.richtext,
                "file": f"/api/v1/asset/?id={p.file.pk}" if p.file else None,
                "memo": p.memo,
            }

        return JsonResponse({
            "product": sc.product.name,
            "code": sc.code,
            "properties": ret,
            })

    def handle_template(self, sc, path):
        if not path:
            path = "index.html"
        asset = sc.product.template_asset
        cache_base = '/var/tmp/emblem-product-cache/'
        os.makedirs(cache_base, exist_ok=True)
        assetd = cache_base + str(asset.pk)
        if not os.path.exists(assetd):
            try:
                self.fetch_template_asset(asset, assetd)
            except:
                # maybe another request races with us, just continue if the
                # files are created concurrently
                if os.path.exists(assetd):
                    pass
                else:
                    raise
        requested_file = os.path.join(assetd, path)
        print(requested_file)
        if not os.path.exists(requested_file):
            return http404()
        return FileResponse(open(requested_file, 'rb'), content_type=self.get_content_type(requested_file))

    def get_content_type(self, fname):
        return mimetypes.guess_type(fname, strict=False)[0]
        
    def fetch_template_asset(self, asset, dest):
        with tempfile.TemporaryDirectory(dir="/var/tmp") as td:
            workd = td + "/target"
            os.makedirs(workd)
            with open(td + "/tmp.zip", 'wb') as f:
                f.write(oss_get(asset.get_name()))
            subprocess.check_output(['unzip', '../tmp.zip'], cwd=workd)
            os.unlink(td + '/tmp.zip')
            os.rename(workd, dest)

class ServiceQrView(BaseView):
    name = 'service-qr'

    def get(self, request):
        tenant_id = request.GET.get("tenant")
        url = 'https://emblem-resources.oss-accelerate.aliyuncs.com/service-qr2.png'
        if not tenant_id:
            return redirect(url)
        else:
            tenant = get_object_or_404(Tenant, pk=tenant_id)
            if tenant.service_qr_file:
                url = oss_sign_url(tenant.service_qr_file.get_name(), "GET")
        return redirect(url)

class StatsView(BaseView):
    name = 'stats'

    def admin_stats(self):
        resp = {
            'total_tenants': Tenant.objects.all().count(),
            'total_products': Product.objects.all().count(),
            'total_scans': ScanData.objects.all().count(),
        }
        return JsonResponse(resp)

    def tenant_stats(self, tenant):
        resp = {
            'total_products': Product.objects.filter(tenant=tenant).count(),
            'total_scans': ScanData.objects.filter(product__tenant=tenant).count(),
            'total_batches': CodeBatch.objects.filter(tenant=tenant).count(),
            'total_codes': SerialCode.objects.filter(batch__tenant=tenant).count(),
        }
        return JsonResponse(resp)

    def get(self, request):
        if request.admin:
            return self.admin_stats()
        if request.tenant:
            return self.tenant_stats(request.tenant)
        return http401()

class AssetView(BaseView):
    name = 'asset'

    @classmethod
    def get_urls(self):
        # Historically we accepted /api/api/v1/asset/ by mistake
        # and it's used as a result of relative paths generated in tiny editor in the frontend
        # So do a loose matching here to accompodate for it
        from django.urls import re_path
        return [
                re_path(r'.*v1/asset/$', AssetView.as_view()),
        ]

    @transaction.atomic
    def post(self, request):
        if not request.admin and not request.tenant:
            return http401()
        p = request.POST
        pk = p.get('id')
        if not pk:
            pk = p.get('pk')
        f = request.FILES.get('file')
        mt = p.get('mime_type', 'application/octet-stream')
        u = str(uuid.uuid4())
        if pk and pk.isnumeric():
            if request.admin:
                obj = get_object_or_404(AssetFile, pk=int(pk))
            else:
                obj = get_object_or_404(AssetFile, pk=int(pk), tenant=request.tenant)
            if mt:
                obj.mime_type = mt
            obj.filename = f.name
            if f:
                obj.uuid = u
            obj.save()
        else:
            obj = AssetFile.objects.create(mime_type=mt, filename=f.name, uuid=u, tenant=request.tenant)
        oss_put(obj.get_name(), f)
        resp = {
            'location': '/api/v1/asset/?id=%d' % obj.pk,
        }
        return JsonResponse(resp)

    def get(self, request):
        pk = request.GET.get("id")
        if not pk or not pk.isnumeric():
            return http404()
        obj = get_object_or_404(AssetFile, pk=int(pk))
        return redirect(oss_sign_url(obj.get_name(), "GET"))

class ImageListView(BaseView):
    name = 'image-list'
    auth_check = 'tenant'

    def get(self, request):
        q = AssetFile.objects.all().filter(tenant=request.tenant)
        ret = []
        for i in q:
            if i.is_image():
                ret.append({'title': i.filename, 'value': '/api/v1/asset/?id=%d' % i.pk})
        return JsonResponse({
            "images": ret,
            })


class OssImageView(BaseView):
    name = 'oss-image'
    auth_check = 'admin'

    def get(self, request):
        name = request.GET.get('name')
        if not name:
            return http404()
        img_bytes = oss_get(name)
        return HttpResponse(img_bytes, content_type='image/jpeg')

class BackupView(BaseView):
    name = 'backup'
    auth_check = 'admin'

    def get(self, request):
        cmd = [MANAGE_PY, 'dumpdata']
        data = subprocess.check_output(cmd)
        return HttpResponse(data, headers={
            'Content-Type': "application/json",
            'Content-Disposition': 'attachment; filename="backup.json"',
            })

class RestoreView(BaseView):
    name = 'restore'
    auth_check = 'admin'

    def post(self, request):
        f = request.FILES.get("file")
        if not f:
            return http400()
        with tempfile.NamedTemporaryFile(suffix="backup.json") as tf:
            for chunk in f.chunks():
                tf.write(chunk)
            tf.flush()
            cmd = [MANAGE_PY, 'loaddata', tf.name]
            subprocess.check_output(cmd)
        return HttpResponse()


class HealthzView(BaseView):
    name = 'healthz'

    def get(self, request):
        return JsonResponse({"status": "running"})

class MyLocationView(BaseView):
    name = 'my-location'
    def get(self, request):
        return JsonResponse({
            'ip': get_client_ip(request),
            'location': request_to_region(request),
            })

class CounterHistoryView(BaseView):
    name = 'counter-history'
    auth_check = 'tenant'

    def align_to_unit(self, t, unit):
        if unit == 'minute':
            return t.replace(microsecond=0, second=0)
        if unit == 'hour':
            return t.replace(microsecond=0, second=0, minute=0)
        if unit == 'day':
            return t.replace(microsecond=0, second=0, minute=0, hour=0)
        return t

    def get(self, request):
        name = request.GET.get('name')
        since = request.GET.get('since')
        until = request.GET.get('until')
        try:
            ts = int(since)
            since_t = datetime.datetime.fromtimestamp(ts, timezone.utc)
            ts = int(until)
            until_t = datetime.datetime.fromtimestamp(ts, timezone.utc)
        except Exception as e:
            return http400()
        params = request.GET.get('params')
        unit = request.GET.get('unit', 'minute')
        if unit == 'minute':
            interval_secs = 60
        elif unit == 'hour':
            interval_secs = 3600
        elif unit == 'day':
            interval_secs = 3600 * 24
        elif unit == 'week':
            interval_secs = 3600 * 24 * 7
        else:
            return http400('invalid unit')

        since_t = self.align_to_unit(since_t, unit)
        total_secs = (until_t - since_t).total_seconds()
        r = get_history_counts(request.tenant, name, since_t, interval_secs, total_secs, params)
        return JsonResponse({
            'data': [(a.timestamp(), b) for a, b in r],
            })

def tid_by_code(sc):
    if sc.tenant:
        return str(sc.tenant.pk)

def mini_prog_entry_redirect(code, tid):
    sc = get_object_or_404(SerialCode, code=code)
    if sc.batch.scan_redirect_url:
        return redirect(sc.batch.scan_redirect_url)
    query = 'tenant=' + str(tid)
    key = 'mini-prog-entry.' + query
    path = sc.batch.mini_prog_entry_path or 'pages/index/index'
    env_version = sc.batch.mini_prog_entry_env_version or 'release'
    if path != "/pages/index/index":
        # the mplink path setting may not work for some reason, so we use
        # redirect param of the index page as a fallback
        query += '&redirect=' + path
    # passing path other than /pages/index/index to mplink may get an error, use
    # the redirect param of the index page as a fallback for now
    url = cache.get_or_set(key, lambda: mplink(query, "/pages/index/index", env_version), 300)
    return redirect(url)

class MiniProgEntryView(BaseView):
    name = 'mini-prog-entry'
    path_prefix = ''

    def get(self, request):
        code = request.GET.get('code')
        if not code:
            return http404()
        sc = get_object_or_404(SerialCode, code=code)
        tid = request.GET.get('tenant') or tid_by_code(sc)
        return mini_prog_entry_redirect(code, tid)

class MiniProgEntryVerifyView(BaseView):
    name = 'mini-prog-entry/aF6EKr9xMu.txt'
    path_prefix = ''

    @classmethod
    def get_urls(self):
        from django.urls import re_path
        return [
                re_path(r'mini-prog-entry/aF6EKr9xMu.txt', MiniProgEntryVerifyView.as_view()),
        ]

    def get(self, request):
        return HttpResponse('bd371601b5b23b418aa3cd1b18dacb51')

class MiniProgEntryVView(View):
    def get(self, request):
        fp = request.get_full_path()
        fs = fp.split('/')
        if len(fs) > 2 and fs[1] == 'v':
            code = fs[2]
        else:
            return http404('pattern not recognized')
        sc = get_object_or_404(SerialCode, code=code)
        tid = request.GET.get('tenant') or tid_by_code(sc)
        return mini_prog_entry_redirect(code, tid)

class MiniProgContentView(BaseView):
    name = 'mini-program-content'

    def post(self, request):
        if not request.tenant and not request.admin:
            return http401()
        mpc, _ = MiniProgramContent.objects.get_or_create(tenant=request.tenant)
        mpc.content = json.dumps(request.data.get("content"))
        mpc.save()
        return JsonResponse({
            'id': mpc.pk,
            })

    def get(self, request):
        pk = request.GET.get('tenant')
        code = request.GET.get('code')
        if code:
            sc = get_object_or_404(SerialCode, code=code)
            if not sc.tenant:
                return http404('code has no tenant')
            pk = str(sc.tenant.pk)
        if not pk or not pk.isnumeric():
            x = get_object_or_404(MiniProgramContent, tenant=None)
        else:
            x = get_object_or_404(MiniProgramContent, tenant__pk=pk)
        data = {
            'id': x.pk,
            'content': x.get_content(),
        }
        return JsonResponse(data)

class CodeBatchOp(object):
    def __init__(self, request):
        op = request.data.get("operation", {})
        self.op = op
        self.op_name = ''
        self.product = None
        self.update_field = []
        if 'product' in op:
            self.product = Product.objects.get(tenant=request.tenant, pk=op['product'])
            self.op_name = '绑定产品'
            self.update_field = 'product'
        if 'unbind' in op:
            self.op_name = '取消绑定'
            self.update_field = 'product'
        if 'is_active' in op:
            val = op['is_active']
            self.update_field = 'is_active'
            if val:
                self.op_name = '激活'
            else:
                self.op_name = '冻结'
        if not self.op_name:
            raise Exception("Invalid op")

    def apply(self, q):
        op = self.op
        if 'product' in op:
            q.update(product=self.product)
        if 'unbind' in op:
            q.update(product=None)
        if 'is_active' in op:
            q.update(is_active=op['is_active'])

class CodeBatchOpView(BaseView):
    name = 'code-batch-op'
    auth_check = 'tenant'

    def post(self, request):
        if not request.tenant:
            raise ValidationError()
        try:
            op = CodeBatchOp(request)
        except Exception as e:
            raise
            return http400(str(e))

        ret = 0
        dry_run = request.data.get("dry_run")
        query = self.make_query(request)
        ret = query.count()
        if dry_run:
            return JsonResponse({
                'count': query.count(),
                })
        job = Job.objects.create(name='batch-op', tenant=request.tenant)
        cmd = [MANAGE_PY, 'codebatchop', '-j', str(job.pk), '-t', str(request.tenant.pk)]
        rd = request.data
        if rd.get('all'):
            cmd += ['--all']
        elif rd.get('seq_range'):
            sr = rd['seq_range']
            cmd += ['--seq-range', "%d,%d" % (sr[0], sr[1])]
        elif rd.get('codes'):
            tf = tempfile.NamedTemporaryFile(delete=False, prefix='code-batch-op', mode='w')
            tf.write('\n'.join(rd['codes']))
            tf.flush()
            cmd += ['--code-file', tf.name]
        reqop = rd['operation']
        if 'product' in reqop:
            cmd += ['--bind-product', reqop['product']]
        elif 'unbind' in reqop:
            cmd += ['--unbind-product']
        elif 'is_active' in reqop:
            if reqop['is_active']:
                cmd += ['--activate']
            else:
                cmd += ['--deactivate']

        cmd = [str(x) for x in cmd]
        print(cmd)
        subprocess.Popen(cmd)

        samples = [x.code for x in query[:10]]
        self.record(request, op, ret, samples)
        return JsonResponse({
            'job_id': job.pk,
            })

    def record(self, request, op, n, code_samples):
        rec = SerialCodeBatchOpRecord(
                tenant=request.tenant,
                batch_size=n)
        rg = request.data.get('seq_range')
        if code_samples:
            rec.code_samples = ", ".join(code_samples)
        if rg:
            rec.seq_num_begin = rg[0]
            rec.seq_num_end = rg[1]
        rec.op_name = op.op_name
        if op.product:
            rec.product_name = op.product.name
            if op.product.article:
                rec.product_page_name = op.product.article.title
        rec.op_name = op.op_name
        rec.save()

    def make_query(self, request):
        tenant = request.tenant
        data = request.data
        q = SerialCode.objects.filter(tenant=tenant)
        if data.get('all'):
            return q
        rg = data.get('seq_range', [])
        if rg:
            return q.filter(seq_num__gte=rg[0], seq_num__lte=rg[1])
        return q.filter(code__in=data.get('codes', []))

class MessageCountView(BaseView):
    name = 'message-count'
    auth_check = 'tenant'

    def get(self, request):
        assert request.admin or request.tenant
        q = UserMessage.objects.filter(admin=request.admin, tenant=request.tenant)
        return JsonResponse({
            'new': q.filter(read=False).count(),
            'total': q.count(),
            })

class SetEmailView(BaseView):
    name = 'set-email'
    auth_check = 'admin'

    def post(self, request):
        u = request.admin.user
        u.email = request.data.get('email')
        u.save()
        return JsonResponse({})

class SetQrVerifyAlertView(BaseView):
    name = 'qr-verify-alert'
    auth_check = 'admin'

    def post(self, request):
        admin = request.admin
        time_window_seconds = request.data.get("time_window_seconds")
        repeat_threshold = request.data.get("repeat_threshold")

        if not isinstance(time_window_seconds, int) or not isinstance(repeat_threshold, int):
            return http400()
        admin.qr_verify_alert_rule = json.dumps({
            'time_window_seconds': int(time_window_seconds),
            'repeat_threshold': int(repeat_threshold),
            })
        admin.save()
        return JsonResponse({})

    def get(self, request):
        ret = json.loads(request.admin.qr_verify_alert_rule or '{}')
        return JsonResponse(ret)

class ManualVerifyRequestView(BaseView):
    name = 'manual-verify-request'

    def post(self, request):
        name = request.data.get("name")
        contact = request.data.get("contact")
        comments = request.data.get("comments", '')
        sdpk = request.data.get("ref")
        if sdpk:
            sd = get_object_or_404(ScanData, pk=sdpk)
            subject = "人工验证请求 (采集失败，扫码记录%d)" % sdpk
            lines = [
                "多次验证失败，用户请求人工验证"
                "",
                "用户姓名： %s" % name,
                "IP： %s" % get_ip(request),
                "位置： %s" % request_to_region(request),
                "序列号： %s" % (sd.code or "n/a"),
                "时间： %s" % sd.datetime,
                "产品： %s" % (sd.product.name if sd.product else "n/a"),
                "租户： %s" % (sd.tenant.username if sd.tenant else "n/a"),
                "图像文件名： %s" % sd.image,
                "扫码记录ID： %d" % sd.pk,
                "",
                "联系方式： %s" % contact,
                "备注： %s" % comments,
                ]
        else:
            subject = "人工验证请求 (客户端未能采集)"
            lines = [
                "无法采集，用户请求人工验证"
                "",
                "用户姓名： %s" % name,
                "IP： %s" % get_ip(request),
                "位置： %s" % request_to_region(request),
                "",
                "联系方式： %s" % contact,
                "备注： %s" % comments,
                ]
        cmd = [MANAGE_PY, 'sendmsg', '-A', '-s', subject, '-c', "\n".join(lines)]
        subprocess.Popen(cmd)
        return JsonResponse({})

class ArticleCssView(BaseView):
    name = 'article.css'

    def get(self, request):
        pk = request.GET.get('id')
        article = get_object_or_404(Article, pk=pk)
        css = gen_article_css(article)
        return HttpResponse(css, content_type='text/css')

    @classmethod
    def get_path(self):
        return self.path_prefix + self.name

class ErrorView(BaseView):
    name = '_error'

    def get(self, request):
        raise Exception("Error view")

class ExportScanDataView(BaseView):
    name = 'export-scan-data'
    auth_check = 'admin'

    def post(self, request):
        hours = request.data.get('hours')
        ts = time.time()
        tf = '/var/tmp/export-scan-data-%s.zip' % ts
        job = Job.objects.create(name='export-scan-data-.%s' % ts, admin=request.admin, output='file://' + tf)
        cmd = [MANAGE_PY, 'exportscandata', '-o', tf, '-j', str(job.pk),
                '--hours', str(hours),
                ]
        subprocess.Popen(cmd)

        return JsonResponse({
            'job': job.pk,
        })

class ScanStatsByProvinceView(BaseView):
    name = 'scan-stats-by-province'
    auth_check = 'tenant'

    def get(self, request):
        try:
            hours = int(request.GET.get('hours'))
        except Exception as e:
            print(e)
            hours = 1
        begin = timezone.now() - datetime.timedelta(hours=hours)
        x = ScanData.objects.filter(datetime__gt=begin, location__isnull=False).values('location').annotate(total=Count('location')).order_by('total')
        ret = defaultdict(int)
        for r in x:
            loc = r['location']
            if '-' in loc:
                prov = loc.split('-')[0]
                ret[prov] += r['total']

        return JsonResponse(ret)

class QrStdView(BaseView):
    name = 'qr-std'
    auth_check = 'admin'

    def get(self, request):
        c = request.GET.get("code")
        qr = self.get_qr_std(c)
        if qr:
            return HttpResponse(qr, content_type="image/jpeg")
        return http404()

def parse_estor_params(headers):
    ret = {}
    for k, v in headers.items():
        if k.startswith("HTTP_X_ESTOR_"):
            try:
                ret[k] = base64.b64decode(v).decode()
            except:
                ret[k] = v
        else:
            ret[k] = v
    return ret

class QrStdUploadView(BaseView):
    name = 'qr-std-upload'
    auth_check = None

    def post(self, request):
        files = request.FILES
        for f in files.values():
            try:
                QrStdUploadView.save_qr(f.name, f.read())
            except Exception as e:
                return http400(f'{f.name}: {str(e)}')
        return JsonResponse({
            'ok': True,
            'nfiles': len(files),
        })

    def save_qr(fname, body):
        basename = os.path.basename(fname)
        name, ext = os.path.splitext(basename)
        if ext != ".jpg":
            raise Exception("invalid file extension")
        if len(body) > 1024 * 1024 * 10:
            raise Exception("file too large")
        if len(name) < 2:
            raise Exception("invalid file name")
        key = code_to_qr_std_key(name)
        oss_put(key, body, bucket=settings.OSS_QRS_BUCKET, endpoint=settings.OSS_QRS_ENDPOINT)

class EstorArchive(BaseView):
    name = 'estor-archive'
    auth_check = None

    def post(self, request):
        rh = parse_estor_params(request.META)
        path = rh.get('HTTP_X_ESTOR_PATH')
        if not path:
            return http400("X-Estor-Path not set")
        path = path.lstrip('/')
        token = rh.get('HTTP_X_ESTOR_PARAM_EMBLEM_TOKEN')
        t = AuthToken.objects.filter(token=token).first()
        if not t or not t.admin:
            return http401()

        created = self.archive_file(path, request.body)
        return JsonResponse({
            'status': 'created' if created else 'existing',
        })

    def archive_file(self, filename, data):
        filename_sha1 = self.get_sha1(filename)
        data_sha1 = self.get_sha1(data)
        er, created = EstorArchiveRecord.objects.get_or_create(filename=filename, filename_sha1=filename_sha1)
        if created or er.data_sha1 != data_sha1:
            oss_put(filename, data, bucket=settings.ARCHIVE_BUCKET)
            er.data_sha1 = data_sha1
            er.save()
            return True
        return False

class SmsVerifyRequestView(BaseView):
    name = 'sms-verify-request'
    auth_check = 'admin'

    def post(self, request):
        try:
            action = request.data['action']
            if not action:
                return http400()
            mobile = request.admin.mobile
            code = str(randint(1000, 9999))
            r = SmsVerifiedAction.objects.create(admin=request.admin, verify_code=code, action=action)
            send_sms_code(mobile, code)
            return JsonResponse({"ok": True, "request_id": r.pk })
        except Exception as e:
            raise
            return JsonResponse({"ok": False, "error": str(e) })

class SmsVerifyCommitView(BaseView):
    name = 'sms-verify-commit'
    auth_check = 'admin'

    def post(self, request):
        try:
            code = request.data['code']
            request_id = request.data['request_id']
            no_older_than = timezone.now() - datetime.timedelta(minutes=10)
            r = get_object_or_404(SmsVerifiedAction, pk=request_id, verify_code=code, admin=request.admin, committed=False, request_time__gte=no_older_than)
            r.committed = True
            r.commit_time = timezone.now()
            r.save()
            return JsonResponse({"ok": True})
        except Exception as e:
            return JsonResponse({"ok": False, "error": str(e) })

class GlobalConfigView(BaseView):
    name = 'global-config'

    def get(self, request):
        name = request.GET.get("name")

        if name == 'camera-zoom':
            # XXX: drop this once the client is switched to CameraRulesView
            q = CameraRule.objects.filter(disabled=False)
            return JsonResponse({
                    "value": make_camera_rules(q)
                }, safe=False)

        value = None
        r = GlobalConfig.objects.filter(name=name).first()
        if r:
            value = r.value
            return JsonResponse({"value": json.loads(value)})
        return http404()

    def post(self, request):
        if not request.admin:
            return http401()
        name = request.data['name']
        value = request.data['value']
        r, _ = GlobalConfig.objects.get_or_create(name=name)
        r.value = json.dumps(value)
        r.save()
        return JsonResponse({"ok": True, "id": r.pk})

class QrStdStatView(BaseView):
    name = 'qr-std-stat'
    auth_check = 'admin'

    def get(self, request):
        ret = oss_stat(bucket=settings.OSS_QRS_BUCKET, endpoint=settings.OSS_QRS_ENDPOINT)
        return JsonResponse(ret)

class LogReportView(BaseView):
    name = 'log-report'

    def post(self, request):
        log = dict(request.data)
        log['ip'] = get_ip(request)
        log['location'] = ip_to_region(log['ip'])
        SystemLog.objects.create(log=json.dumps(log))
        return JsonResponse({})

class DebugUploadView(BaseView):
    name = 'debug-upload'

    def post(self, request):
        image, fn = get_image(request)
        if not image:
            return JsonResponse({"error": "image not found in request"})
        qrcode = request.data.get('qrcode').split("code=")[-1]
        phonemodel = request.data.get('phonemodel')
        phonemodel = phonemodel.split("<")[0].strip().replace(" ", "")
        r = GlobalConfig.objects.filter(name='debug_upload_dir').first()
        if r:
            upload_dir = r.get_value()
        else:
            upload_dir = "default"
        save_dir = "/emblem/data/debug-upload/{}/{}".format(upload_dir, phonemodel)
        if not os.path.exists(save_dir):
            os.makedirs(save_dir)

        r = GlobalConfig.objects.filter(name='debug_upload_neg_or_pos').first()
        if r:
            neg_or_pos = r.get_value();
        else:
            neg_or_pos = "unknown"
        with tempfile.NamedTemporaryFile(suffix=fn) as tf:
            tf.write(image)
            tf.flush()
            ts = time.time()
            save_file = os.path.join(save_dir, "{}_{}_{}_{}.jpg".format(qrcode, neg_or_pos, phonemodel, ts))
            url = 'http://alg:3028/alg/rectify'
            with open(tf.name, 'rb') as tfr:
                r = requests.post(url, data=tfr)
                r.raise_for_status()
                with open(save_file, 'wb') as sfw:
                    sfw.write(r.content)
        return JsonResponse({"info": "ok"})

def make_camera_rules(q):
    def make_camera_config_obj(cc):
        return {
            'model': cc.model_text,
            'zoom': cc.zoom,
            'web_view': cc.use_web_view,
        }
    return [make_camera_config_obj(cc) for cc in q]

class CameraRulesView(BaseView):
    name = 'camera-rules'

    def get(self, request):
        q = CameraRule.objects.filter(disabled=False)
        return JsonResponse(make_camera_rules(q), safe=False)

class CameraFrameView(BaseView):
    '''
    handle camera frame upload
    '''
    name = 'camera-frame'

    def post(self, request):
        filebody, filename = get_image(request)
        date = timezone.now().strftime('%Y-%m-%d')
        session_id = request.data.get("session_id", "unknown-session")
        phone_model = request.data.get('phone_model', 'unknown phone')
        seq_num = request.data.get('seq_num', time.time())
        key = f'{date}/{phone_model}-{session_id}-{seq_num}-{filename}'
        oss_put(key, filebody, bucket=settings.FRAMES_BUCKET)
        return JsonResponse({})

class FramesView(BaseView):
    '''
    get frame list
    '''
    name = 'frames'
    auth_check = 'admin'

    def get(self, request):
        start = request.GET.get('start', 0)
        end = request.GET.get('end', 100)
        filter_label = request.GET.get('label', None)
        ret = sorted(oss_list(bucket=settings.FRAMES_BUCKET), reverse=True)
        start = int(start)
        end = int(end)
        frames = []
        for frame in ret[start:end]:
            fo = FrameLabel.objects.filter(oss_path=frame).first()
            if fo:
                if filter_label and filter_label not in fo.labels.split(','):
                    continue
                frames.append({
                    'path': frame,
                    'labels': fo.labels.split(','),
                })
            elif not filter_label:
                frames.append({
                    'path': frame,
                    'labels': [],
                })
        return JsonResponse({
            'frames': frames,
            'total': len(ret),
            'start': start,
            'end': end,
        })
class FrameView(BaseView):
    '''
    get one frame image from oss
    '''
    name = 'frame'

    def get(self, request):
        path = request.GET.get('path')
        return HttpResponse(oss_get(path, bucket=settings.FRAMES_BUCKET),
                            content_type='image/jpeg')
class DeleteFrameView(BaseView):
    '''
    delete one frame from oss
    '''
    name = 'delete-frame'
    auth_check = 'admin'

    def post(self, request):
        path = request.data.get('path')
        oss_delete(path, bucket=settings.FRAMES_BUCKET)
        FrameLabel.objects.filter(oss_path=path).delete()
        return JsonResponse({})

class LabelFrameView(BaseView):
    name = 'label-frame'
    auth_check = 'admin'

    def post(self, request):
        path = request.data.get('path')
        label = request.data.get('label')
        fo, _ = FrameLabel.objects.get_or_create(oss_path=path)
        if fo.labels.split(',').count(label) == 0:
            fo.labels = fo.labels + ',' + label if fo.labels else label
        else:
            fo.labels = ','.join([x for x in fo.labels.split(',') if x != label])
        fo.save()
        return JsonResponse({
            'ok': True,
            'id': fo.id,
        })

class ABTestView(BaseView):
    name = 'ab-test'
    auth_check = None

    def get(self, request):
        test_object = request.GET.get('test_object', None)
        abtest = ABTest.objects.filter(enabled=True)
        if test_object:
            abtest = abtest.filter(test_object=test_object)
        return JsonResponse({
            'items': [
                {
                    'name': abtest.name,
                    'test_object': abtest.test_object,
                    'spec': abtest.spec,
                }
                for abtest in abtest
            ]
        })

class ABTestReportView(BaseView):
    name = 'ab-test-report'
    auth_check = None

    def post(self, request):
        name = request.data.get('name')
        abtest = ABTest.objects.filter(name=name).first()
        if not abtest:
            return http404()
        sample = ABTestSample.objects.create(abtest=abtest, data=json.dumps(request.data.get('data')))
        return JsonResponse({
            'ok': True,
        })

class ScanDataLabelsView(BaseView):
    name = 'scan-data-labels'
    auth_check = 'admin'

    def get(self, request):
        ret = []
        for x in ScanData.objects.all().filter(labels__isnull=False).order_by('pk').values('id', 'labels', 'image', 'code', 'succeeded'):
            if not x.get('labels'):
                continue
            ret.append({
                'id': x['id'],
                'labels': x.get('labels'),
                'code': x.get('code'),
                'image': x.get('image'),
                'succeeded': x.get('succeeded'),
            })
        return JsonResponse({
            'items': ret,
        })


class AIChatView(BaseView):
    """AI聊天API视图 - 仅支持发送消息，临时会话模式"""
    name = 'ai-chat'
    auth_check = None
    
    def post(self, request):
        """发送消息给AI并获取回复"""
        try:
            # 获取请求参数
            session_id = request.data.get('session_id')
            message = request.data.get('message')
            context = request.data.get('context', {})
            chat_type = request.data.get('chat_type', 'platform')
            product_id = request.data.get('product_id')
            
            # 验证必需参数
            if not message:
                return JsonResponse({
                    'error': 'message is required'
                }, status=400)
            
            # 获取或创建会话
            session = self._get_or_create_session(session_id, request.tenant, product_id)
            
            # 确定聊天类型和产品ID
            if chat_type == 'product' and product_id:
                ai_chat_type = 'product'
                ai_product_id = product_id
            else:
                ai_chat_type = 'platform'
                ai_product_id = None
            
            # 创建AI服务实例
            ai_service = AIChatService(
                chat_type=ai_chat_type,
                product_id=ai_product_id
            )
            
            # 调用AI服务
            response = ai_service.chat(message)
            
            # 保存消息记录
            self._save_message(session, 'user', message)
            self._save_message(session, 'assistant', response)
            
            return JsonResponse({
                'session_id': session.session_id,
                'response': response,
                'chat_type': ai_chat_type,
                'product_id': ai_product_id
            })
            
        except Exception as e:
            return JsonResponse({
                'error': f'AI chat failed: {str(e)}'
            }, status=500)
    
    
    def _get_or_create_session(self, session_id, tenant, product_id=None):
        """获取或创建聊天会话"""
        if session_id:
            # 尝试获取现有会话
            session = ChatSession.objects.filter(
                session_id=session_id,
                tenant=tenant
            ).first()
            if session:
                return session
        
        # 创建新会话
        if not session_id:
            session_id = str(uuid.uuid4())
        
        session = ChatSession.objects.create(
            session_id=session_id,
            tenant=tenant,
            product_id=product_id
        )
        
        return session
    
    def _save_message(self, session, role, content, message_type='text'):
        """保存消息到数据库"""
        ChatMessage.objects.create(
            session=session,
            role=role,
            message_type=message_type,
            text_content=content
        )


class KnowledgeBaseSearchView(BaseView):
    """知识库搜索API - 直接检索知识库内容"""
    name = 'knowledge-base-search'
    auth_check = None
    
    def post(self, request):
        """搜索知识库"""
        try:
            # 获取请求参数
            query = request.data.get('query')
            max_results = request.data.get('max_results', 5)
            tenant_id = request.data.get('tenant_id')
            
            # 验证必需参数
            if not query:
                return JsonResponse({
                    'error': 'query is required'
                }, status=400)
            
            # 验证 max_results
            if not isinstance(max_results, int) or max_results < 1 or max_results > 20:
                return JsonResponse({
                    'error': 'max_results must be an integer between 1 and 20'
                }, status=400)
            
            # 导入 RAG 服务
            try:
                from .rag_service import CachedLangChainRAG
            except ImportError as e:
                return JsonResponse({
                    'error': f'RAG service not available: {str(e)}'
                }, status=500)
            
            # 创建 RAG 服务实例
            try:
                rag_service = CachedLangChainRAG()
            except Exception as e:
                return JsonResponse({
                    'error': f'Failed to initialize RAG service: {str(e)}'
                }, status=500)
            
            # 执行搜索
            try:
                results = rag_service.search(
                    query=query,
                    max_results=max_results,
                    tenant_id=tenant_id
                )
                
                return JsonResponse({
                    'query': query,
                    'max_results': max_results,
                    'tenant_id': tenant_id,
                    'results': results,
                    'count': len(results)
                })
                
            except Exception as e:
                import traceback
                traceback.print_exc()
                return JsonResponse({
                    'error': f'Search failed: {str(e)}'
                }, status=500)
            
        except Exception as e:
            import traceback
            traceback.print_exc()
            return JsonResponse({
                'error': f'Request processing failed: {str(e)}'
            }, status=500)
    
    def get(self, request):
        """获取知识库搜索使用说明"""
        return JsonResponse({
            'name': 'Knowledge Base Search API',
            'description': 'Search the knowledge base for relevant information',
            'method': 'POST',
            'parameters': {
                'query': {
                    'type': 'string',
                    'required': True,
                    'description': 'The search query'
                },
                'max_results': {
                    'type': 'integer',
                    'required': False,
                    'default': 5,
                    'range': '1-20',
                    'description': 'Maximum number of results to return'
                },
                'tenant_id': {
                    'type': 'integer',
                    'required': False,
                    'description': 'Tenant ID for tenant-specific search'
                }
            },
            'response': {
                'query': 'The search query',
                'max_results': 'Maximum results requested',
                'tenant_id': 'Tenant ID (if provided)',
                'results': [
                    {
                        'article_id': 'Article ID',
                        'title': 'Article title',
                        'content': 'Relevant content snippet',
                        'score': 'Relevance score',
                        'metadata': 'Additional metadata'
                    }
                ],
                'count': 'Number of results returned'
            },
            'example': {
                'request': {
                    'query': '支持多语言吗',
                    'max_results': 3
                }
            }
        })
    
