#!/usr/bin/env python3
# /var/www/html/y/yyy_sed.py
# DATA_PY_PATH.'/yyy_sed.py';
# 용도 : sed.php에서 호출 하여 ai가 sed 기능을 쓸수있게 하기위함.
# 지원 :
# 1) exact 문자열 치환
# 2) 여러 줄 exact 치환
# 3) regex=1 정규식 치환
# 4) icase=1 대소문자 무시
# 5) limit=1 등 치환 횟수 제한
# 6) count_only=1 일 때 개수만 확인
import os
import sys
import json
import html
import shutil
import difflib
import tempfile
import re
from datetime import datetime


def jprint(obj):
    sys.stdout.write(json.dumps(obj, ensure_ascii=False))


def read_text(path):
    with open(path, 'r', encoding='utf-8', errors='replace', newline='') as f:
        return f.read()


def write_text(path, text):
    with open(path, 'w', encoding='utf-8', newline='') as f:
        f.write(text)


def build_backup_path(src_path, backup_root):
    ts = datetime.now().strftime('%Y%m%d__%H_%M_%S')
    rel = src_path.lstrip('/')
    return os.path.join(backup_root, rel + '_' + ts + '.bak')


def build_unified(old_txt, new_txt, file_label):
    a = old_txt.splitlines()
    b = new_txt.splitlines()
    diff = difflib.unified_diff(a, b, fromfile=file_label, tofile=file_label, lineterm='')
    return '\n'.join(diff)


def colorize_unified(diff_text):
    out = []
    for line in diff_text.split('\n'):
        esc = html.escape(line)
        if line.startswith('+++') or line.startswith('---') or line.startswith('@@'):
            out.append(f'<div style="color:#7dd3fc">{esc}</div>')
        elif line.startswith('+'):
            out.append(f'<div style="background:#052e16;color:#86efac">{esc}</div>')
        elif line.startswith('-'):
            out.append(f'<div style="background:#450a0a;color:#fca5a5">{esc}</div>')
        else:
            out.append(f'<div>{esc}</div>')
    return '\n'.join(out)


def html_wrap(body, file_path, replace_count, run, regex=False, icase=False, limit=None, count_only=False, body_is_html=True):
    title = 'sed 미리보기' if not run else 'sed 적용 결과'
    if count_only:
        title = 'sed 개수 확인'

    if not body_is_html:
        body = html.escape(body)

    limit_text = '' if limit is None else str(limit)

    return f"""<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{html.escape(title)}</title>
</head>
<body style="margin:0;padding:16px;background:#111;color:#eee;font-family:Consolas,Monaco,monospace;">
  <div style="font-size:18px;font-weight:700;margin-bottom:8px;">{html.escape(title)}</div>
  <div style="color:#bbb;margin-bottom:6px;">file: {html.escape(file_path)}</div>
  <div style="color:#bbb;margin-bottom:6px;">replace_count: {replace_count}</div>
  <div style="color:#bbb;margin-bottom:6px;">regex: {1 if regex else 0}</div>
  <div style="color:#bbb;margin-bottom:6px;">icase: {1 if icase else 0}</div>
  <div style="color:#bbb;margin-bottom:6px;">count_only: {1 if count_only else 0}</div>
  <div style="color:#bbb;margin-bottom:14px;">limit: {html.escape(limit_text)}</div>
  <div style="border:1px solid #333;border-radius:8px;background:#1b1b1b;padding:12px;white-space:pre-wrap;word-break:break-word;">{body}</div>
</body>
</html>"""


def parse_bool(v):
    if isinstance(v, bool):
        return v
    if v is None:
        return False
    return str(v).strip() in ('1', 'true', 'True', 'yes', 'on')


def parse_limit(v):
    if v is None or v == '':
        return None
    try:
        n = int(v)
        if n < 1:
            return None
        return n
    except Exception:
        return None


def compile_pattern(from_str, regex_mode=False, icase=False):
    flags = re.MULTILINE
    if regex_mode:
        flags |= re.DOTALL
    if icase:
        flags |= re.IGNORECASE

    if regex_mode:
        return re.compile(from_str, flags)

    return re.compile(re.escape(from_str), flags | (re.IGNORECASE if icase else 0))


def count_matches(pattern, text):
    return sum(1 for _ in pattern.finditer(text))


def replace_with_pattern(pattern, text, to_str, limit=None):
    if limit is None:
        new_txt, replaced = pattern.subn(to_str, text)
        return new_txt, replaced

    new_txt, replaced = pattern.subn(to_str, text, count=limit)
    return new_txt, replaced


def main():
    try:
        req = json.load(sys.stdin)

        file_path = (req.get('file') or '').strip()
        from_str = req.get('from')
        to_str = req.get('to', '')
        run = parse_bool(req.get('run', False))
        fmt = (req.get('fmt') or 'html').strip().lower()

        backup_root = (req.get('backup_root') or '/data/_bak/_sed_').rstrip('/')
        icase = parse_bool(req.get('icase', False))
        regex_mode = parse_bool(req.get('regex', False))
        count_only = parse_bool(req.get('count_only', False))
        limit = parse_limit(req.get('limit'))

        if fmt not in ('html', 'text', 'json'):
            fmt = 'html'

        if not file_path:
            return jprint({
                'ok': False,
                'error': 'file 누락'
            })

        if from_str is None or from_str == '':
            return jprint({
                'ok': False,
                'error': 'from 누락'
            })

        if not os.path.isfile(file_path):
            return jprint({
                'ok': False,
                'error': '파일 없음',
                'file': file_path
            })

        old_txt = read_text(file_path)

        try:
            pattern = compile_pattern(from_str, regex_mode=regex_mode, icase=icase)
        except re.error as e:
            return jprint({
                'ok': False,
                'error': '정규식 오류',
                'regex': True,
                'regex_error': str(e),
                'file': file_path
            })

        total_match_count = count_matches(pattern, old_txt)

        if count_only:
            message = '개수 확인 완료'
            output_text = (
                f'file: {file_path}\n'
                f'replace_count: {total_match_count}\n'
                f'regex: {1 if regex_mode else 0}\n'
                f'icase: {1 if icase else 0}\n'
                f'count_only: 1\n'
                f'limit: {"" if limit is None else limit}\n'
            )

            if fmt == 'html':
                output = html_wrap(
                    html.escape(output_text),
                    file_path,
                    total_match_count,
                    False,
                    regex=regex_mode,
                    icase=icase,
                    limit=limit,
                    count_only=True,
                    body_is_html=True
                )
            elif fmt == 'text':
                output = output_text
            else:
                output = output_text

            return jprint({
                'ok': True,
                'run': False,
                'changed': False,
                'count_only': True,
                'regex': regex_mode,
                'icase': icase,
                'limit': limit,
                'replace_count': total_match_count,
                'total_match_count': total_match_count,
                'message': message,
                'file': file_path,
                'output': output
            })

        new_txt, applied_replace_count = replace_with_pattern(pattern, old_txt, to_str, limit=limit)

        diff_text = build_unified(old_txt, new_txt, file_path)
        if not diff_text.strip():
            diff_text = '(변경 없음)'

        if not run:
            if fmt == 'text':
                preview = diff_text + '\n'
            elif fmt == 'html':
                preview = html_wrap(
                    colorize_unified(diff_text),
                    file_path,
                    applied_replace_count,
                    False,
                    regex=regex_mode,
                    icase=icase,
                    limit=limit,
                    count_only=False,
                    body_is_html=True
                )
            else:
                preview = diff_text + '\n'

            return jprint({
                'ok': True,
                'run': False,
                'regex': regex_mode,
                'icase': icase,
                'count_only': False,
                'limit': limit,
                'changed': applied_replace_count > 0,
                'replace_count': applied_replace_count,
                'total_match_count': total_match_count,
                'file': file_path,
                'output': preview
            })

        if applied_replace_count < 1:
            return jprint({
                'ok': True,
                'run': True,
                'regex': regex_mode,
                'icase': icase,
                'count_only': False,
                'limit': limit,
                'changed': False,
                'replace_count': 0,
                'total_match_count': total_match_count,
                'message': '변경 없음',
                'backup_file': '',
                'file': file_path
            })

        backup_file = build_backup_path(file_path, backup_root)
        os.makedirs(os.path.dirname(backup_file), exist_ok=True)
        shutil.copy2(file_path, backup_file)

        fd, tmp_path = tempfile.mkstemp(prefix='.sedtmp_', dir=os.path.dirname(file_path))
        os.close(fd)

        try:
            write_text(tmp_path, new_txt)
            shutil.copymode(file_path, tmp_path)
            os.replace(tmp_path, file_path)
        finally:
            if os.path.exists(tmp_path):
                try:
                    os.unlink(tmp_path)
                except Exception:
                    pass

        return jprint({
            'ok': True,
            'run': True,
            'regex': regex_mode,
            'icase': icase,
            'count_only': False,
            'limit': limit,
            'changed': True,
            'replace_count': applied_replace_count,
            'total_match_count': total_match_count,
            'message': '수정 완료',
            'backup_file': backup_file,
            'file': file_path
        })

    except Exception as e:
        return jprint({
            'ok': False,
            'error': str(e)
        })


if __name__ == '__main__':
    main()